diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 12ad4e452b1c7..2b1720ccaabae 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,11 +5,12 @@ - Information on your environment, - Steps to reproduce, - Expected and actual results, + Fields marked with (*) are required. Please don't remove the template. Please also have a look at our guidelines article before adding a new issue https://github.com/magento/magento2/wiki/Issue-reporting-guidelines --> -### Preconditions +### Preconditions (*) 1. [Screenshots, logs or description] -### Actual result +### Actual result (*) 1. [Screenshots, logs or description] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 17aa66c919eb5..33a6ef02ace11 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,28 +6,29 @@ about: Technical issue with the Magento 2 core components -### Preconditions +### Preconditions (*) 1. 2. -### Steps to reproduce +### Steps to reproduce (*) 1. 2. -### Expected result +### Expected result (*) 1. [Screenshots, logs or description] 2. -### Actual result +### Actual result (*) 1. [Screenshots, logs or description] 2. diff --git a/.github/ISSUE_TEMPLATE/developer-experience-issue.md b/.github/ISSUE_TEMPLATE/developer-experience-issue.md index a66b0c62ef8e2..423d4818fb31c 100644 --- a/.github/ISSUE_TEMPLATE/developer-experience-issue.md +++ b/.github/ISSUE_TEMPLATE/developer-experience-issue.md @@ -6,12 +6,13 @@ about: Issues related to customization, extensibility, modularity -### Summary +### Summary (*) -### Examples +### Examples (*) ### Proposed solution diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index de85da43b70fa..f64185773cab4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,13 +5,13 @@ about: Please consider reporting directly to https://github.com/magento/communit --- -### Description +### Description (*) -### Expected behavior +### Expected behavior (*) ### Benefits diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5b0b9d74e453b..f191bd9aaba67 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,12 +3,13 @@ To help us process this pull request we recommend that you add the following information: - Summary of the pull request, - Issue(s) related to the changes made, - - Manual testing scenarios, + - Manual testing scenarios + Fields marked with (*) are required. Please don't remove the template. --> -### Description +### Description (*) + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
- +
diff --git a/app/code/Magento/Backend/Test/Unit/Block/DataProviders/UploadConfigTest.php b/app/code/Magento/Backend/Test/Unit/Block/DataProviders/UploadConfigTest.php new file mode 100644 index 0000000000000..481c08aed4a01 --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Block/DataProviders/UploadConfigTest.php @@ -0,0 +1,65 @@ +config = $this->getMockBuilder(ScopeConfig::class) + ->setMethods(['getValue']) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->uploadConfig = $this->objectManagerHelper->getObject( + UploadConfig::class, + [ + 'config' => $this->config + ] + ); + } + + /** + * @dataProvider configValuesDataProvider() + * @param int $configValue + * @param int $expectedValue + * @return void + */ + public function testGetIsResizeEnabled(int $configValue, int $expectedValue) + { + $this->config->expects($this->once()) + ->method('getValue') + ->with('system/upload_configuration/enable_resize') + ->willReturn($configValue); + $this->assertEquals($expectedValue, $this->uploadConfig->getIsResizeEnabled()); + } + + public function configValuesDataProvider(): array + { + return [ + [1, 1], + [0, 0] + ]; + } +} diff --git a/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php new file mode 100644 index 0000000000000..c7ff1d95617b6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php @@ -0,0 +1,71 @@ +moduleListMock = $this->createMock(ModuleListInterface::class); + $this->objectManager = new ObjectManager($this); + $this->moduleService = $this->objectManager->getObject( + ModuleService::class, + [ + 'moduleList' => $this->moduleListMock, + ] + ); + } + + /** + * Test getModules method + * + * @return void + */ + public function testGetModules() + { + $moduleNames = ['Magento_Backend', 'Magento_Catalog', 'Magento_Customer']; + $this->moduleListMock->expects($this->once())->method('getNames')->willReturn($moduleNames); + + $expected = $moduleNames; + $actual = $this->moduleService->getModules(); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index ecf11ac601861..e3411166ee4a4 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -322,15 +322,26 @@ - + + + Magento\Config\Model\Config\Source\Yesno + Resize performed via javascript before file upload. + + validate-greater-than-zero validate-number required-entry Maximum allowed width for uploaded image. + + 1 + - + validate-greater-than-zero validate-number required-entry Maximum allowed height for uploaded image. + + 1 + diff --git a/app/code/Magento/Backend/etc/config.xml b/app/code/Magento/Backend/etc/config.xml index 45d283ad3ff22..8283fa18dd370 100644 --- a/app/code/Magento/Backend/etc/config.xml +++ b/app/code/Magento/Backend/etc/config.xml @@ -29,6 +29,7 @@ 1 + 1 1920 1200 diff --git a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml index 966372773f295..50cfdc4e07681 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml @@ -14,7 +14,8 @@ "Magento_Backend/js/media-uploader" : { "maxFileSize": getFileSizeService()->getMaxFileSize() ?>, "maxWidth":getImageUploadMaxWidth() ?> , - "maxHeight": getImageUploadMaxHeight() ?> + "maxHeight": getImageUploadMaxHeight() ?>, + "isResizeEnabled": getImageUploadConfigData()->getIsResizeEnabled() ?> } }' > diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index 03403a4ec4a04..34e388bac3d33 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -25,9 +25,20 @@ define([ * @private */ _create: function () { - var - self = this, - progressTmpl = mageTemplate('[data-template="uploader"]'); + var self = this, + progressTmpl = mageTemplate('[data-template="uploader"]'), + isResizeEnabled = this.options.isResizeEnabled, + resizeConfiguration = { + action: 'resize', + maxWidth: this.options.maxWidth, + maxHeight: this.options.maxHeight + }; + + if (!isResizeEnabled) { + resizeConfiguration = { + action: 'resize' + }; + } this.element.find('input[type=file]').fileupload({ dataType: 'json', @@ -44,8 +55,7 @@ define([ * @param {Object} data */ add: function (e, data) { - var - fileSize, + var fileSize, tmpl; $.each(data.files, function (index, file) { @@ -115,11 +125,9 @@ define([ process: [{ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/ - }, { - action: 'resize', - maxWidth: this.options.maxWidth, - maxHeight: this.options.maxHeight - }, { + }, + resizeConfiguration, + { action: 'save' }] }); diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php index a035c84b4cafd..4d63ee4125b74 100644 --- a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php @@ -49,6 +49,8 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $data = $payment->getAdditionalInformation(); + // the payment token could be stored only if a customer checks the Vault flow on storefront + // see https://developers.braintreepayments.com/guides/paypal/vault/javascript/v2#invoking-the-vault-flow if (!empty($data[VaultConfigProvider::IS_ACTIVE_CODE])) { $result[self::$optionsKey] = [ self::$storeInVaultOnSuccess => true diff --git a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php index 4280663178efb..5b773e03c8870 100644 --- a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php @@ -6,6 +6,7 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Braintree\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; @@ -41,6 +42,9 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $extensionAttributes = $payment->getExtensionAttributes(); $paymentToken = $extensionAttributes->getVaultPaymentToken(); + if ($paymentToken === null) { + throw new CommandException(__('The Payment Token is not available to perform the request.')); + } return [ 'amount' => $this->formatPrice($this->subjectReader->readAmount($buildSubject)), 'paymentMethodToken' => $paymentToken->getGatewayToken() diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php index fe5895541543d..aa23fa767d1ed 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php @@ -148,7 +148,7 @@ private function updateBillingAddress(Quote $quote, array $details) { $billingAddress = $quote->getBillingAddress(); - if ($this->config->isRequiredBillingAddress()) { + if ($this->config->isRequiredBillingAddress() && !empty($details['billingAddress'])) { $this->updateAddressData($billingAddress, $details['billingAddress']); } else { $this->updateAddressData($billingAddress, $details['shippingAddress']); diff --git a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php index e06b913db8ef4..4e422c795095f 100644 --- a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php @@ -41,12 +41,14 @@ public function __construct(Config $config, ResolverInterface $resolver) } /** - * Retrieve assoc array of checkout configuration + * Retrieve assoc array of checkout configuration. * * @return array */ - public function getConfig() + public function getConfig(): array { + $requireBillingAddressAll = \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL; + return [ 'payment' => [ self::PAYPAL_CODE => [ @@ -60,6 +62,8 @@ public function getConfig() 'vaultCode' => self::PAYPAL_VAULT_CODE, 'skipOrderReview' => $this->config->isSkipOrderReview(), 'paymentIcon' => $this->config->getPayPalIcon(), + 'isRequiredBillingAddress' => + (int)$this->config->isRequiredBillingAddress() === $requireBillingAddressAll, ] ] ]; diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php index 5af050002eb2d..80d333db80f0a 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php @@ -3,16 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Braintree\Test\Unit\Gateway\Request; -use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Api\Data\OrderPaymentExtension; use Magento\Sales\Model\Order\Payment; use Magento\Vault\Model\PaymentToken; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Tests VaultCaptureDataBuilder. + */ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase { /** @@ -30,7 +34,15 @@ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase */ private $payment; - public function setUp() + /** + * @var SubjectReader|MockObject + */ + private $subjectReader; + + /** + * @inheritdoc + */ + protected function setUp() { $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); $this->payment = $this->getMockBuilder(Payment::class) @@ -39,11 +51,15 @@ public function setUp() $this->paymentDO->method('getPayment') ->willReturn($this->payment); - $this->builder = new VaultCaptureDataBuilder(new SubjectReader()); + $this->subjectReader = $this->getMockBuilder(SubjectReader::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->builder = new VaultCaptureDataBuilder($this->subjectReader); } /** - * \Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder::build + * Checks the result after builder execution. */ public function testBuild() { @@ -51,19 +67,28 @@ public function testBuild() $token = '5tfm4c'; $buildSubject = [ 'payment' => $this->paymentDO, - 'amount' => $amount + 'amount' => $amount, ]; $expected = [ 'amount' => $amount, - 'paymentMethodToken' => $token + 'paymentMethodToken' => $token, ]; + $this->subjectReader->method('readPayment') + ->with($buildSubject) + ->willReturn($this->paymentDO); + $this->subjectReader->method('readAmount') + ->with($buildSubject) + ->willReturn($amount); + + /** @var OrderPaymentExtension|MockObject $paymentExtension */ $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) ->setMethods(['getVaultPaymentToken']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + /** @var PaymentToken|MockObject $paymentToken */ $paymentToken = $this->getMockBuilder(PaymentToken::class) ->disableOriginalConstructor() ->getMock(); @@ -79,4 +104,39 @@ public function testBuild() $result = $this->builder->build($buildSubject); self::assertEquals($expected, $result); } + + /** + * Checks a builder execution if Payment Token doesn't exist. + * + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage The Payment Token is not available to perform the request. + */ + public function testBuildWithoutPaymentToken(): void + { + $amount = 30.00; + $buildSubject = [ + 'payment' => $this->paymentDO, + 'amount' => $amount, + ]; + + $this->subjectReader->method('readPayment') + ->with($buildSubject) + ->willReturn($this->paymentDO); + $this->subjectReader->method('readAmount') + ->with($buildSubject) + ->willReturn($amount); + + /** @var OrderPaymentExtension|MockObject $paymentExtension */ + $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) + ->setMethods(['getVaultPaymentToken']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->payment->method('getExtensionAttributes') + ->willReturn($paymentExtension); + $paymentExtension->method('getVaultPaymentToken') + ->willReturn(null); + + $this->builder->build($buildSubject); + } } diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index 39863e6561c43..7475d81a56142 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -3,22 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Model\Paypal\Helper; +use Magento\Braintree\Gateway\Config\PayPal\Config; +use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; +use Magento\Braintree\Observer\DataAssignObserver; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartExtensionInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Payment; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; -use Magento\Braintree\Observer\DataAssignObserver; -use Magento\Braintree\Gateway\Config\PayPal\Config; -use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class QuoteUpdaterTest * - * @see \Magento\Braintree\Model\Paypal\Helper\QuoteUpdater - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase @@ -26,39 +28,42 @@ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase const TEST_NONCE = '3ede7045-2aea-463e-9754-cd658ffeeb48'; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ - private $configMock; + private $config; /** - * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartRepositoryInterface|MockObject */ - private $quoteRepositoryMock; + private $quoteRepository; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $billingAddressMock; + private $billingAddress; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $shippingAddressMock; + private $shippingAddress; /** * @var QuoteUpdater */ private $quoteUpdater; + /** + * @inheritdoc + */ protected function setUp() { - $this->configMock = $this->getMockBuilder(Config::class) + $this->config = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->quoteRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) + $this->quoteRepository = $this->getMockBuilder(CartRepositoryInterface::class) ->getMockForAbstractClass(); - $this->billingAddressMock = $this->getMockBuilder(Address::class) + $this->billingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -73,9 +78,10 @@ protected function setUp() 'setShouldIgnoreValidation', 'getEmail' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); - $this->shippingAddressMock = $this->getMockBuilder(Address::class) + $this->shippingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -89,54 +95,61 @@ protected function setUp() 'setPostcode', 'setShouldIgnoreValidation' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); $this->quoteUpdater = new QuoteUpdater( - $this->configMock, - $this->quoteRepositoryMock + $this->config, + $this->quoteRepository ); } + /** + * Checks if quote details can be update by the response from Braintree. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecute() { $details = $this->getDetails(); - $quoteMock = $this->getQuoteMock(); - $paymentMock = $this->getPaymentMock(); + $quote = $this->getQuoteMock(); + $payment = $this->getPaymentMock(); - $quoteMock->expects(self::once()) - ->method('getPayment') - ->willReturn($paymentMock); + $quote->method('getPayment') + ->willReturn($payment); - $paymentMock->expects(self::once()) - ->method('setMethod') + $payment->method('setMethod') ->with(ConfigProvider::PAYPAL_CODE); - $paymentMock->expects(self::once()) - ->method('setAdditionalInformation') + $payment->method('setAdditionalInformation') ->with(DataAssignObserver::PAYMENT_METHOD_NONCE, self::TEST_NONCE); - $this->updateQuoteStep($quoteMock, $details); + $this->updateQuoteStep($quote, $details); - $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quoteMock); + $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quote); } + /** + * Disables quote's addresses validation. + * + * @return void + */ private function disabledQuoteAddressValidationStep() { - $this->billingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->billingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->shippingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->shippingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->billingAddressMock->expects(self::once()) - ->method('getEmail') + $this->billingAddress->method('getEmail') ->willReturn('bt_buyer_us@paypal.com'); } /** + * Gets quote's details. + * * @return array */ - private function getDetails() + private function getDetails(): array { return [ 'email' => 'bt_buyer_us@paypal.com', @@ -166,54 +179,51 @@ private function getDetails() } /** + * Updates shipping address details. + * * @param array $details */ private function updateShippingAddressStep(array $details) { - $this->shippingAddressMock->expects(self::once()) - ->method('setLastname') + $this->shippingAddress->method('setLastname') ->with($details['lastName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setFirstname') + $this->shippingAddress->method('setFirstname') ->with($details['firstName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setEmail') + $this->shippingAddress->method('setEmail') ->with($details['email']); - $this->shippingAddressMock->expects(self::once()) - ->method('setCollectShippingRates') + $this->shippingAddress->method('setCollectShippingRates') ->with(true); - $this->updateAddressDataStep($this->shippingAddressMock, $details['shippingAddress']); + $this->updateAddressDataStep($this->shippingAddress, $details['shippingAddress']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $addressMock + * Updates address details. + * + * @param MockObject $address * @param array $addressData */ - private function updateAddressDataStep(\PHPUnit_Framework_MockObject_MockObject $addressMock, array $addressData) + private function updateAddressDataStep(MockObject $address, array $addressData) { - $addressMock->expects(self::once()) - ->method('setStreet') + $address->method('setStreet') ->with([$addressData['streetAddress'], $addressData['extendedAddress']]); - $addressMock->expects(self::once()) - ->method('setCity') + $address->method('setCity') ->with($addressData['locality']); - $addressMock->expects(self::once()) - ->method('setRegionCode') + $address->method('setRegionCode') ->with($addressData['region']); - $addressMock->expects(self::once()) - ->method('setCountryId') + $address->method('setCountryId') ->with($addressData['countryCodeAlpha2']); - $addressMock->expects(self::once()) - ->method('setPostcode') + $address->method('setPostcode') ->with($addressData['postalCode']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote's address details. + * + * @param MockObject $quoteMock * @param array $details */ - private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteAddressStep(MockObject $quoteMock, array $details) { $quoteMock->expects(self::exactly(2)) ->method('getIsVirtual') @@ -224,82 +234,92 @@ private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject } /** + * Updates billing address details. + * * @param array $details */ private function updateBillingAddressStep(array $details) { - $this->configMock->expects(self::once()) - ->method('isRequiredBillingAddress') + $this->config->method('isRequiredBillingAddress') ->willReturn(true); - $this->updateAddressDataStep($this->billingAddressMock, $details['billingAddress']); + $this->updateAddressDataStep($this->billingAddress, $details['billingAddress']); - $this->billingAddressMock->expects(self::once()) - ->method('setLastname') + $this->billingAddress->method('setLastname') ->with($details['lastName']); - $this->billingAddressMock->expects(self::once()) - ->method('setFirstname') + $this->billingAddress->method('setFirstname') ->with($details['firstName']); - $this->billingAddressMock->expects(self::once()) - ->method('setEmail') + $this->billingAddress->method('setEmail') ->with($details['email']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote details. + * + * @param MockObject $quote * @param array $details */ - private function updateQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteStep(MockObject $quote, array $details) { - $quoteMock->expects(self::once()) - ->method('setMayEditShippingAddress') + $quote->method('setMayEditShippingAddress') ->with(false); - $quoteMock->expects(self::once()) - ->method('setMayEditShippingMethod') + $quote->method('setMayEditShippingMethod') ->with(true); - $quoteMock->expects(self::exactly(2)) - ->method('getShippingAddress') - ->willReturn($this->shippingAddressMock); - $quoteMock->expects(self::exactly(2)) + $quote->method('getShippingAddress') + ->willReturn($this->shippingAddress); + $quote->expects(self::exactly(2)) ->method('getBillingAddress') - ->willReturn($this->billingAddressMock); + ->willReturn($this->billingAddress); - $this->updateQuoteAddressStep($quoteMock, $details); + $this->updateQuoteAddressStep($quote, $details); $this->disabledQuoteAddressValidationStep(); - $quoteMock->expects(self::once()) - ->method('collectTotals'); + $quote->method('collectTotals'); - $this->quoteRepositoryMock->expects(self::once()) - ->method('save') - ->with($quoteMock); + $this->quoteRepository->method('save') + ->with($quote); } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Quote object. + * + * @return Quote|MockObject */ - private function getQuoteMock() + private function getQuoteMock(): MockObject { - return $this->getMockBuilder(Quote::class) + $quote = $this->getMockBuilder(Quote::class) ->setMethods( [ 'getIsVirtual', 'getPayment', + 'getExtensionAttributes', 'setMayEditShippingAddress', 'setMayEditShippingMethod', 'collectTotals', 'getShippingAddress', 'getBillingAddress', ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); + + $cartExtension = $this->getMockBuilder(CartExtensionInterface::class) + ->setMethods(['setShippingAssignments']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $quote->method('getExtensionAttributes') + ->willReturn($cartExtension); + return $quote; } /** - * @return Payment|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Payment object. + * + * @return Payment|MockObject */ - private function getPaymentMock() + private function getPaymentMock(): MockObject { return $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php index 22f7f46bd98f1..d3dba400507f6 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php @@ -47,9 +47,10 @@ protected function setUp() } /** - * Run test getConfig method + * Run test getConfig method. * * @param array $expected + * @return void * @dataProvider getConfigDataProvider */ public function testGetConfig($expected) @@ -77,13 +78,16 @@ public function testGetConfig($expected) 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' ]); + $this->config->method('isRequiredBillingAddress') + ->willReturn(1); + self::assertEquals($expected, $this->configProvider->getConfig()); } /** * @return array */ - public function getConfigDataProvider() + public function getConfigDataProvider(): array { return [ [ @@ -101,7 +105,8 @@ public function getConfigDataProvider() 'skipOrderReview' => false, 'paymentIcon' => [ 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' - ] + ], + 'isRequiredBillingAddress' => true, ] ] ] diff --git a/app/code/Magento/Braintree/i18n/en_US.csv b/app/code/Magento/Braintree/i18n/en_US.csv index 6bf677151ed0d..7bd305f546dc6 100644 --- a/app/code/Magento/Braintree/i18n/en_US.csv +++ b/app/code/Magento/Braintree/i18n/en_US.csv @@ -192,3 +192,4 @@ Currency,Currency "Too many concurrent attempts to refund this transaction. Try again later.","Too many concurrent attempts to refund this transaction. Try again later." "Too many concurrent attempts to void this transaction. Try again later.","Too many concurrent attempts to void this transaction. Try again later." "Braintree Settlement","Braintree Settlement" +"The Payment Token is not available to perform the request.","The Payment Token is not available to perform the request." diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index 253f3530701bc..c60504f2c44ce 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -206,7 +206,9 @@ define([ beforePlaceOrder: function (data) { this.setPaymentMethodNonce(data.nonce); - if (quote.billingAddress() === null && typeof data.details.billingAddress !== 'undefined') { + if ((this.isRequiredBillingAddress() || quote.billingAddress() === null) && + typeof data.details.billingAddress !== 'undefined' + ) { this.setBillingAddress(data.details, data.details.billingAddress); } @@ -264,6 +266,14 @@ define([ return window.checkoutConfig.payment[this.getCode()].isAllowShippingAddressOverride; }, + /** + * Is billing address required from PayPal side. + * @returns {Boolean} + */ + isRequiredBillingAddress: function () { + return window.checkoutConfig.payment[this.getCode()].isRequiredBillingAddress; + }, + /** * Get configuration for PayPal * @returns {Object} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml new file mode 100644 index 0000000000000..72140cf6d5848 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + 4.99 + + + 2.89 + + + 7.33 + + + 18.25 + + + + {{productName}} + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4.99 + + + 2.89 + + + 7.33 + + + 18.25 + + + + {{productName}} + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml new file mode 100644 index 0000000000000..fda5d10295676 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index c55b0166b04ba..ecdc4da3a75b6 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> Api Bundle Product api-bundle-product @@ -37,4 +37,19 @@ CustomAttributeDynamicPrice CustomAttributePriceViewRange + + Api Fixed Bundle Product + api-fixed-bundle-product + bundle + 4 + 1.23 + 4 + 1 + api-fixed-bundle-product + EavStockItem + ApiProductDescription + ApiProductShortDescription + CustomAttributeFixPrice + CustomAttributePriceView + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml new file mode 100644 index 0000000000000..46ac1cdae93c0 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -0,0 +1,78 @@ + + + + + + + + + + <description value="Add Bundle product with zero price to shopping cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-83535"/> + <group value="bundle"/> + </annotations> + <before> + <!--Enable freeShipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + <!--Create simple with zero price product--> + <createData entity="ApiProductWithDescription" stepKey="apiSimple"> + <field key="price">0</field> + </createData> + <!--Create Bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="apiBundleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <!--Create Attribute--> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="apiBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink"> + <requiredEntity createDataKey="apiBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="apiSimple"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <deleteData createDataKey="apiSimple" stepKey="deleteSimple"/> + <deleteData createDataKey="apiBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open category page--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <!--Add bundle product to cart--> + <actionGroup ref="StorefrontAddCategoryBundleProductToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="product" value="$$apiBundleProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!--Place order--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!--Check subtotal in created order--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$0.00" stepKey="checkSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php index 9c7604d27eae0..2b7074e4c26d2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -16,8 +16,11 @@ use Magento\Framework\Data\Form\Element\AbstractElement; /** + * Attributes tab block + * * @api * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements @@ -33,6 +36,9 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements */ protected $_attributeAction; + /** @var array */ + private $excludeFields; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -40,6 +46,7 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction * @param array $data + * @param array $excludeFields */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -47,14 +54,19 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction, - array $data = [] + array $data = [], + array $excludeFields = [] ) { $this->_attributeAction = $attributeAction; $this->_productFactory = $productFactory; + $this->excludeFields = $excludeFields; + parent::__construct($context, $registry, $formFactory, $data); } /** + * Construct block + * * @return void */ protected function _construct() @@ -64,20 +76,14 @@ protected function _construct() } /** + * Prepares form + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _prepareForm() { - $this->setFormExcludedFieldList( - [ - 'category_ids', - 'gallery', - 'image', - 'media_gallery', - 'quantity_and_stock_status', - 'tier_price', - ] - ); + $this->setFormExcludedFieldList($this->getExcludedFields()); $this->_eventManager->dispatch( 'adminhtml_catalog_product_form_prepare_excluded_field_list', ['object' => $this] @@ -155,6 +161,8 @@ protected function _getAdditionalElementHtml($element) } /** + * Returns tab label + * * @return \Magento\Framework\Phrase */ public function getTabLabel() @@ -163,6 +171,8 @@ public function getTabLabel() } /** + * Return Tab title + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -171,6 +181,8 @@ public function getTabTitle() } /** + * Can show tab in tabs + * * @return bool */ public function canShowTab() @@ -179,10 +191,22 @@ public function canShowTab() } /** + * Tab not hidden + * * @return bool */ public function isHidden() { return false; } + + /** + * Returns excluded fields + * + * @return array + */ + private function getExcludedFields(): array + { + return $this->excludeFields; + } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index ac9e75493bdc3..dd7554a9b7b19 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -17,9 +17,16 @@ use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Block\DataProviders\UploadConfig as ImageUploadConfigDataProvider; class Content extends \Magento\Backend\Block\Widget { + /** + * @var ImageUploadConfigDataProvider + */ + private $imageUploadConfigDataProvider; + /** * @var string */ @@ -45,16 +52,20 @@ class Content extends \Magento\Backend\Block\Widget * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param array $data + * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, - array $data = [] + array $data = [], + ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; parent::__construct($context, $data); + $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider + ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); } /** @@ -62,7 +73,11 @@ public function __construct( */ protected function _prepareLayout() { - $this->addChild('uploader', \Magento\Backend\Block\Media\Uploader::class); + $this->addChild( + 'uploader', + \Magento\Backend\Block\Media\Uploader::class, + ['image_upload_config_data' => $this->imageUploadConfigDataProvider] + ); $this->getUploader()->getConfig()->setUrl( $this->_urlBuilder->addSessionParam()->getUrl('catalog/product_gallery/upload') diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php index 09c3a3864bbc7..b11696ed0d129 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php @@ -102,7 +102,7 @@ private function getCategoryIds(Filter $filter): array } } - return array_unique(array_merge($categoryIds, ...$childCategoryIds)); + return array_map('intval', array_unique(array_merge($categoryIds, ...$childCategoryIds))); } /** diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index 7599e46f76e30..7f047327460ca 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -69,14 +69,9 @@ class ImageUploader /** * List of allowed image mime types * - * @var array + * @var string[] */ - private $allowedMimeTypes = [ - 'image/jpg', - 'image/jpeg', - 'image/gif', - 'image/png' - ]; + private $allowedMimeTypes; /** * ImageUploader constructor @@ -89,6 +84,7 @@ class ImageUploader * @param string $baseTmpPath * @param string $basePath * @param string[] $allowedExtensions + * @param string[] $allowedMimeTypes */ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, @@ -98,7 +94,8 @@ public function __construct( \Psr\Log\LoggerInterface $logger, $baseTmpPath, $basePath, - $allowedExtensions + $allowedExtensions, + $allowedMimeTypes = [] ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); @@ -108,6 +105,7 @@ public function __construct( $this->baseTmpPath = $baseTmpPath; $this->basePath = $basePath; $this->allowedExtensions = $allowedExtensions; + $this->allowedMimeTypes = $allowedMimeTypes; } /** @@ -167,7 +165,7 @@ public function getBasePath() } /** - * Retrieve base path + * Retrieve allowed extensions * * @return string[] */ diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index fbe0d4b550fa6..abe1b3eedbc84 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -341,6 +341,12 @@ protected function _updateTemporaryTableByStoreValues( if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } + + /* + * According to \Magento\Framework\DB\SelectRendererInterface select rendering may be updated + * so we need to trigger select renderer for correct update + */ + $select->assemble(); $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); } @@ -355,6 +361,7 @@ protected function _updateTemporaryTableByStoreValues( if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } + $select->assemble(); $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index e4dacf275fc9b..8a9233f176c61 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -985,7 +985,7 @@ public function setQty($qty) */ public function getQty() { - return $this->getData('qty'); + return (float)$this->getData('qty'); } /** diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php index c2108b0273bdb..dbefb09f06bd1 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php @@ -25,7 +25,9 @@ public function beforeSave($object) $attributeCode = $this->getAttribute()->getName(); if ($object->getData('use_config_' . $attributeCode)) { $object->setData($attributeCode, BooleanSource::VALUE_USE_CONFIG); + return $this; } - return $this; + + return parent::beforeSave($object); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php index a9da2a3400de9..752007aced094 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php @@ -97,6 +97,7 @@ protected function _generateUniqueSku($object) public function beforeSave($object) { $this->_generateUniqueSku($object); + $this->trimValue($object); return parent::beforeSave($object); } @@ -127,4 +128,17 @@ protected function _getLastSimilarAttributeValueIncrement($attribute, $object) $data = $connection->fetchOne($select, $bind); return abs((int)str_replace($value, '', $data)); } + + /** + * @param Product $object + * @return void + */ + private function trimValue($object) + { + $attrCode = $this->getAttribute()->getAttributeCode(); + $value = $object->getData($attrCode); + if ($value) { + $object->setData($attrCode, trim($value)); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index f26e55099b054..9a4f3892fe115 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -16,7 +16,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler { /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processDeletedImages($product, array &$images) @@ -31,7 +32,7 @@ protected function processDeletedImages($product, array &$images) foreach ($images as &$image) { if (!empty($image['removed'])) { - if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { + if (!empty($image['value_id'])) { if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { continue; } @@ -52,7 +53,8 @@ protected function processDeletedImages($product, array &$images) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processNewImage($product, array &$image) @@ -79,6 +81,8 @@ protected function processNewImage($product, array &$image) } /** + * Retrieve store ids from product. + * * @param \Magento\Catalog\Model\Product $product * @return array * @since 101.0.0 @@ -97,6 +101,8 @@ protected function extractStoreIds($product) } /** + * Remove deleted images. + * * @param array $files * @return null * @since 101.0.0 diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 74e5e94889253..2463734503d88 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -36,7 +36,7 @@ class Image extends \Magento\Framework\Model\AbstractModel * * @var int */ - protected $_quality = 80; + protected $_quality = null; /** * @var bool @@ -294,7 +294,8 @@ public function setQuality($quality) */ public function getQuality() { - return $this->_quality; + return $this->_quality === null + ? $this->_scopeConfig->getValue('system/upload_configuration/jpeg_quality') : $this->_quality; } /** @@ -471,7 +472,7 @@ public function getImageProcessor() $this->_processor->keepTransparency($this->_keepTransparency); $this->_processor->constrainOnly($this->_constrainOnly); $this->_processor->backgroundColor($this->_backgroundColor); - $this->_processor->quality($this->_quality); + $this->_processor->quality($this->getQuality()); return $this->_processor; } @@ -854,7 +855,7 @@ private function getMiscParams() 'constrain_only' => ($this->_constrainOnly ? 'do' : 'not') . 'constrainonly', 'background' => $this->_rgbToString($this->_backgroundColor), 'angle' => $this->_angle, - 'quality' => $this->_quality, + 'quality' => $this->getQuality(), ]; // if has watermark add watermark params to hash diff --git a/app/code/Magento/Catalog/Model/Product/Url.php b/app/code/Magento/Catalog/Model/Product/Url.php index c291dc33fedab..a295f3fbe1a0f 100644 --- a/app/code/Magento/Catalog/Model/Product/Url.php +++ b/app/code/Magento/Catalog/Model/Product/Url.php @@ -7,6 +7,7 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Product Url model @@ -45,6 +46,11 @@ class Url extends \Magento\Framework\DataObject */ protected $urlFinder; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param \Magento\Framework\UrlFactory $urlFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -52,6 +58,7 @@ class Url extends \Magento\Framework\DataObject * @param \Magento\Framework\Session\SidResolverInterface $sidResolver * @param UrlFinderInterface $urlFinder * @param array $data + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Framework\UrlFactory $urlFactory, @@ -59,7 +66,8 @@ public function __construct( \Magento\Framework\Filter\FilterManager $filter, \Magento\Framework\Session\SidResolverInterface $sidResolver, UrlFinderInterface $urlFinder, - array $data = [] + array $data = [], + ScopeConfigInterface $scopeConfig = null ) { parent::__construct($data); $this->urlFactory = $urlFactory; @@ -67,16 +75,8 @@ public function __construct( $this->filter = $filter; $this->sidResolver = $sidResolver; $this->urlFinder = $urlFinder; - } - - /** - * Retrieve URL Instance - * - * @return \Magento\Framework\UrlInterface - */ - private function getUrlInstance() - { - return $this->urlFactory->create(); + $this->scopeConfig = $scopeConfig ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -157,10 +157,19 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) UrlRewrite::ENTITY_TYPE => \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE, UrlRewrite::STORE_ID => $storeId, ]; - if ($categoryId) { + $useCategories = $this->scopeConfig->getValue( + \Magento\Catalog\Helper\Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + if ($useCategories && $categoryId) { $filterData[UrlRewrite::METADATA]['category_id'] = $categoryId; + } elseif (!$useCategories) { + $filterData[UrlRewrite::METADATA]['category_id'] = ''; } + $rewrite = $this->urlFinder->findOneByData($filterData); + if ($rewrite) { $requestPath = $rewrite->getRequestPath(); $product->setRequestPath($requestPath); @@ -194,6 +203,8 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) $routeParams['_query'] = []; } - return $this->getUrlInstance()->setScope($storeId)->getUrl($routePath, $routeParams); + $url = $this->urlFactory->create()->setScope($storeId); + + return $url->getUrl($routePath, $routeParams); } } diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php index 68d2c0606e567..f65d9b4c0bf7a 100644 --- a/app/code/Magento/Catalog/Model/ProductCategoryList.php +++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php @@ -81,7 +81,10 @@ public function getCategoryIds($productId) Select::SQL_UNION_ALL ); - $this->categoryIdList[$productId] = $this->productResource->getConnection()->fetchCol($unionSelect); + $this->categoryIdList[$productId] = array_map( + 'intval', + $this->productResource->getConnection()->fetchCol($unionSelect) + ); } return $this->categoryIdList[$productId]; diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index dd941680b8a4d..fb6e8a6c8a311 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -18,6 +18,7 @@ use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\DB\Adapter\DeadlockException; use Magento\Framework\DB\Adapter\LockWaitException; +use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; @@ -26,6 +27,7 @@ use Magento\Framework\Exception\ValidatorException; /** + * Product Repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -155,6 +157,11 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa */ private $serializer; + /** + * @var ReadExtensions + */ + private $readExtensions; + /** * ProductRepository constructor. * @param ProductFactory $productFactory @@ -180,6 +187,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa * @param CollectionProcessorInterface $collectionProcessor [optional] * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param int $cacheLimit [optional] + * @param ReadExtensions|null $readExtensions * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -206,7 +214,8 @@ public function __construct( \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, - $cacheLimit = 1000 + $cacheLimit = 1000, + ReadExtensions $readExtensions = null ) { $this->productFactory = $productFactory; $this->collectionFactory = $collectionFactory; @@ -229,10 +238,12 @@ public function __construct( $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); $this->cacheLimit = (int)$cacheLimit; + $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ReadExtensions::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { @@ -249,11 +260,12 @@ public function get($sku, $editMode = false, $storeId = null, $forceReload = fal $this->cacheProduct($cacheKey, $product); $cachedProduct = $product; } + return $cachedProduct; } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { @@ -272,6 +284,7 @@ public function getById($productId, $editMode = false, $storeId = null, $forceRe } $this->cacheProduct($cacheKey, $product); } + return $this->instancesById[$productId][$cacheKey]; } @@ -292,6 +305,7 @@ protected function getCacheKey($data) } } $serializeData = $this->serializer->serialize($serializeData); + return sha1($serializeData); } @@ -349,6 +363,8 @@ protected function initializeProductData(array $productData, $createNew) } /** + * Assign product to websites. + * * @param \Magento\Catalog\Model\Product $product * @return void */ @@ -418,11 +434,12 @@ private function processLinks(ProductInterface $product, $newLinks) } $product->setProductLinks($newLinks); + return $this; } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -494,7 +511,7 @@ public function save(ProductInterface $product, $saveOptions = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(ProductInterface $product) { @@ -513,20 +530,22 @@ public function delete(ProductInterface $product) } $this->removeProductFromLocalCache($sku); unset($this->instancesById[$productId]); + return true; } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($sku) { $product = $this->get($sku); + return $this->delete($product); } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { @@ -543,6 +562,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $collection->load(); $collection->addCategoryIds(); + $this->addExtensionAttributes($collection); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); @@ -553,7 +573,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $this->getCacheKey( [ false, - $product->hasData(\Magento\Catalog\Model\Product::STORE_ID) ? $product->getStoreId() : null + $product->getStoreId() ] ), $product @@ -563,6 +583,21 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr return $searchResult; } + /** + * Add extension attributes to loaded items. + * + * @param Collection $collection + * @return Collection + */ + private function addExtensionAttributes(Collection $collection): Collection + { + foreach ($collection->getItems() as $item) { + $this->readExtensions->execute($item); + } + + return $collection; + } + /** * Helper function that adds a FilterGroup to the collection. * @@ -608,6 +643,8 @@ public function cleanCache() } /** + * Retrieve media gallery processor. + * * @return ProductRepository\MediaGalleryProcessor */ private function getMediaGalleryProcessor() @@ -616,6 +653,7 @@ private function getMediaGalleryProcessor() $this->mediaGalleryProcessor = \Magento\Framework\App\ObjectManager::getInstance() ->get(ProductRepository\MediaGalleryProcessor::class); } + return $this->mediaGalleryProcessor; } @@ -632,6 +670,7 @@ private function getCollectionProcessor() 'Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor' ); } + return $this->collectionProcessor; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 937b21a9c8421..749a3d754570f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -1422,6 +1422,11 @@ protected function _addUrlRewrite() ['cu' => $this->getTable('catalog_url_rewrite_product_category')], 'u.url_rewrite_id=cu.url_rewrite_id' )->where('cu.category_id IN (?)', $this->_urlRewriteCategory); + } else { + $select->joinLeft( + ['cu' => $this->getTable('catalog_url_rewrite_product_category')], + 'u.url_rewrite_id=cu.url_rewrite_id' + )->where('cu.url_rewrite_id IS NULL'); } // more priority is data with category id @@ -1803,7 +1808,8 @@ protected function _productLimitationJoinWebsite() } $conditions[] = $this->getConnection()->quoteInto( 'product_website.website_id IN(?)', - $filters['website_ids'] + $filters['website_ids'], + 'int' ); } elseif (isset( $filters['store_id'] @@ -1815,7 +1821,7 @@ protected function _productLimitationJoinWebsite() ) { $joinWebsite = true; $websiteId = $this->_storeManager->getStore($filters['store_id'])->getWebsiteId(); - $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId); + $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId, 'int'); } $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); @@ -2011,12 +2017,12 @@ protected function _applyProductLimitations() $conditions = [ 'cat_index.product_id=e.entity_id', - $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id']), + $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id'], 'int'), ]; if (isset($filters['visibility']) && !isset($filters['store_table'])) { - $conditions[] = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility']); + $conditions[] = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility'], 'int'); } - $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id']); + $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id'], 'int'); if (isset($filters['category_is_anchor'])) { $conditions[] = $this->getConnection()->quoteInto('cat_index.is_parent=?', $filters['category_is_anchor']); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index 8428ff3688b28..0005ac8dea58a 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -190,7 +190,7 @@ public function getQuery(array $dimensions, string $productType, array $entityId $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; $specialToExpr = "{$specialTo} IS NULL OR {$specialToDate} >= {$currentDate}"; $specialPriceExpr = $connection->getCheckSql( - "{$specialPrice} IS NOT NULL AND {$specialFromExpr} AND {$specialToExpr}", + "{$specialPrice} IS NOT NULL AND ({$specialFromExpr}) AND ({$specialToExpr})", $specialPrice, $maxUnsignedBigint ); diff --git a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php index 54a13be864db7..77368517a3155 100644 --- a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php @@ -30,7 +30,7 @@ public function getValue() $this->value = false; foreach ($this->priceInfo->getPrices() as $price) { if ($price instanceof BasePriceProviderInterface && $price->getValue() !== false) { - $this->value = min($price->getValue(), $this->value ?: $price->getValue()); + $this->value = min($price->getValue(), $this->value !== false ? $this->value: $price->getValue()); } } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml index b1300f63d967c..5c71342264212 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml @@ -25,4 +25,17 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitingForWarningModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreDelete"/> </actionGroup> + + <actionGroup name="navigateToProductAttribute"> + <arguments> + <argument name="attributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAttributeGridPageLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.filterByAttributeCode}}" userInput="{{attributeCode}}" stepKey="fillAttributeCodeFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductAttributeGridSection.attributeCode(attributeCode)}}" stepKey="navigateToAttributeEditPage" /> + <waitForPageLoad stepKey="waitForAttributeEditPageLoad" /> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index cbc3ac876a486..1c4e7a8df2436 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -91,6 +91,7 @@ <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> </actionGroup> + <!--Set product to website--> <actionGroup name="ProductSetWebsite"> <arguments> <argument name="website"/> @@ -131,13 +132,41 @@ <!--Switch to New Store view--> <actionGroup name="SwitchToTheNewStoreView"> <arguments> - <argument name="storeViewName" type="string"/> + <argument name="storeViewName"/> </arguments> - <scrollTo selector="{{AdminProductContentSection.pageHeader}}" stepKey="scrollToUp"/> + <scrollTo selector="{{AdminHeaderSection.pageTitle}}" stepKey="scrollToUp"/> <waitForElementVisible selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="waitForElementBecomeVisible"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView(storeViewName)}}" stepKey="chooseStoreView"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(storeViewName.name)}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <!--Select Product In Websites--> + <actionGroup name="SelectProductInWebsitesActionGroup"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenProductInWebsite"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + </actionGroup> + + <!--Remove product image--> + <actionGroup name="RemoveProductImage"> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesSection"/> + <waitForPageLoad time="30" stepKey="waitForPageRefresh"/> + <click selector="{{AdminProductImagesSection.removeImageButton}}" stepKey="clickRemoveImage"/> + </actionGroup> + + <!-- Assert no product image in Admin Product page --> + <actionGroup name="AssertProductImageNotInAdminProductPage"> + <arguments> + <argument name="image" defaultValue="MagentoLogo" /> + </arguments> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dontSeeElement selector="{{AdminProductImagesSection.imageFile(image.filename)}}" stepKey="seeImage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml new file mode 100644 index 0000000000000..341360ce87173 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="navigateToEditProductAttribute"> + <arguments> + <argument name="attributeLabel" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.gridFilterFrontEndLabel}}" userInput="{{attributeLabel}}" stepKey="navigateToAttributeEditPage1" /> + <click selector="{{AdminProductAttributeGridSection.search}}" stepKey="navigateToAttributeEditPage2" /> + <click selector="{{AdminProductAttributeGridSection.firstRow}}" stepKey="navigateToAttributeEditPage3" /> + </actionGroup> + <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> + <arguments> + <argument name="useForPromoRule" type="string" defaultValue="Yes"/> + </arguments> + <click selector="{{StorefrontPropertiesSection.storefrontPropertiesTab}}" stepKey="clickStoreFrontPropertiesTab"/> + <selectOption selector="{{StorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="{{useForPromoRule}}" stepKey="changeOption"/> + <click selector="{{AttributePropertiesSection.save}}" stepKey="saveAttribute"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the product attribute." stepKey="successMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml new file mode 100644 index 0000000000000..fc2a8964ea302 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.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="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKey"> + <arguments> + <argument name="product"/> + </arguments> + <!-- Go to storefront product page, assert product name and sku --> + <amOnPage url="{{product.custom_attributes[url_key]}}.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInTitle userInput="{{product.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 1f9bd218b1111..8f0df2fc899a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Check the category page --> <actionGroup name="StorefrontCheckCategoryActionGroup"> <arguments> @@ -35,4 +35,16 @@ <click selector="{{StorefrontCategoryMainSection.modeListButton}}" stepKey="switchCategoryViewToListMode"/> <seeElement selector="{{StorefrontCategoryMainSection.categoryTitle}}" stepKey="assertCategoryTitle"/> </actionGroup> + + <!-- Go to storefront category product page by given parameters --> + <actionGroup name="GoToStorefrontCategoryPageByParameters"> + <arguments> + <argument name="category" type="string"/> + <argument name="mode" type="string"/> + <argument name="sortBy" type="string" defaultValue="position"/> + <argument name="sort" type="string" defaultValue="asc"/> + </arguments> + <!-- Go to storefront category page --> + <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_mode={{mode}}&product_list_order={{sortBy}}&product_list_dir={{sort}}" stepKey="onCategoryPage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml new file mode 100644 index 0000000000000..67e71cc3b9e92 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogAttributeSet" type="CatalogAttributeSet"> + <data key="attribute_set_name" unique="suffix">test_set_</data> + <data key="attributeGroupId">7</data> + <data key="skeletonId">4</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml index 2423383bc19f7..77cb6e13a95e3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -7,9 +7,13 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="ProductAttributeFrontendLabel" type="FrontendLabel"> <data key="store_id">0</data> <data key="label" unique="suffix">attribute</data> </entity> + <entity name="ProductAttributeFrontendLabelThree" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">attributeThree</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 35e6d198c7f54..007529a06d9f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -249,4 +249,18 @@ <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> </entity> + <entity name="SimpleProductWithCustomAttributeSet" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <var key="attribute_set_id" entityKey="attribute_set_id" entityType="CatalogAttributeSet"/> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="weight">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml new file mode 100644 index 0000000000000..00a5f8fef18f1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="AddCatalogAttributeToAttributeSet" dataType="CatalogAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets" method="POST"> + <contentType>application/json</contentType> + <object key="attributeSet" dataType="CatalogAttributeSet"> + <field key="attribute_set_name">string</field> + <field key="sort_order">integer</field> + </object> + <field key="skeletonId">integer</field> + </operation> + <operation name="DeleteCatalogAttributeFromAttributeSet" dataType="CatalogAttributeSet" type="delete" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetCatalogAttributesFromDefaultSet" dataType="CatalogAttributeSet" type="get" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml index 3ac0645fa13d2..1bf7d0b0d988f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml @@ -7,7 +7,7 @@ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateProduct" dataType="product" type="create" auth="adminOauth" url="/V1/products" method="POST"> <contentType>application/json</contentType> <object dataType="product" key="product"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml index 3c90f32990b7d..09544d05a9b56 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_link-meta.xml @@ -7,16 +7,16 @@ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateProductLink" dataType="product_link" type="create"> <field key="sku">string</field> <field key="link_type">string</field> <field key="linked_product_sku">string</field> <field key="linked_product_type">string</field> <field key="position">integer</field> - <array key="extension_attributes"> - <value>product_link_extension_attribute</value> - </array> + <object key="extension_attributes" dataType="product_link_extension_attribute"> + <field key="qty">integer</field> + </object> </operation> <operation name="UpdateProductLink" dataType="product_link" type="update"> <field key="sku">string</field> @@ -24,8 +24,8 @@ <field key="linked_product_sku">string</field> <field key="linked_product_type">string</field> <field key="position">integer</field> - <array key="extension_attributes"> - <value>product_link_extension_attribute</value> - </array> + <object key="extension_attributes" dataType="product_link_extension_attribute"> + <field key="qty">integer</field> + </object> </operation> </operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml new file mode 100644 index 0000000000000..3a8999b523f13 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product_links-meta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateProductLinks" dataType="product_links" type="create" auth="adminOauth" url="/V1/products/{sku}/links" method="POST"> + <contentType>application/json</contentType> + <array key="items"> + <value>product_link</value> + </array> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml index a11d05140c638..45485509f5e3e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml @@ -14,5 +14,6 @@ <section name="AdminCategoryBasicFieldSection"/> <section name="AdminCategorySEOSection"/> <section name="AdminCategoryModalSection"/> + <section name="AdminCategoryProductsGridSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml index 3d7e79343155b..e7de264dc2b75 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeEditPage.xml @@ -11,5 +11,6 @@ <page name="AdminProductAttributeEditPage" url="catalog/product_attribute/edit/" area="admin" module="Magento_Catalog"> <section name="AdminProductAttributeEditSection"/> <section name="AdminConfirmationModalSection"/> + <section name="AdminEditAttributeStorefrontPropertiesSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml index a5de7453d9c23..3a9bd1647a476 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeGridPage.xml @@ -9,5 +9,6 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> <page name="AdminProductAttributeGridPage" url="catalog/product_attribute" area="admin" module="Catalog"> <section name="AdminProductAttributeGridSection"/> + <section name="AdminDataGridHeaderSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml new file mode 100644 index 0000000000000..61a4c497d5b3a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminCategoryProductsGridSection"> + <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml index e5e37b59af359..e241370220921 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -7,9 +7,13 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AttributePropertiesSection"> <element name="AdvancedProperties" type="button" selector="#advanced_fieldset-wrapper"/> <element name="Save" type="button" selector="#save"/> </section> + <section name="StorefrontPropertiesSection"> + <element name="storefrontPropertiesTab" selector="#product_attribute_tabs_front" type="button" timeout="30"/> + <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml new file mode 100644 index 0000000000000..e430998f1e2be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditAttributeStorefrontPropertiesSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminEditAttributeStorefrontPropertiesSection"> + <element name="storeFrontPropertiesTab" selector="#product_attribute_tabs_front" type="button"/> + <element name="useForPromoRuleConditions" type="select" selector="#is_used_for_promo_rules"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index bb944270a10a9..020ae95b11c2b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -14,7 +14,8 @@ <element name="gridFilterFrontEndLabel" type="input" selector="#attributeGrid_filter_frontend_label"/> <element name="search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> - <element name="firstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]"/> + <element name="firstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]" timeout="30"/> <element name="attributeCodeFilterInput" type="input" selector=".admin__data-grid-filters input[name='attribute_code']"/> + <element name="filterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml index 55a1892389416..fbf07429b8408 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AttributePropertiesSection.xml @@ -14,7 +14,7 @@ <element name="valueRequired" type="select" selector="#is_required"/> <element name="advancedProperties" type="button" selector="#advanced_fieldset-wrapper"/> <element name="defaultValue" type="input" selector="#default_value_text"/> - <element name="save" type="button" selector="#save"/> + <element name="save" type="button" selector="#save" timeout="30"/> <element name="saveAndEdit" type="button" selector="#save_and_edit_button"/> <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index f793cd14441f7..1f1a4ce9133e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCategoryMainSection"> <element name="modeListButton" type="button" selector="#mode-list" timeout="10"/> <element name="categoryTitle" type="text" selector="#page-title-heading span"/> @@ -21,5 +21,6 @@ <element name="productsList" type="text" selector="//ol[@class='products list items product-items']"/> <element name="categoryPageProductImagePlaceholderSmall" type="text" selector=".products-grid img[src*='placeholder/small_image.jpg']"/> <element name="categoryPageProductImage" type="text" selector=".products-grid img[src*='/{{var1}}']" parameterized="true"/> + <element name="categoryPageProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml new file mode 100644 index 0000000000000..2431d626387e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminFilteringCategoryProductsUsingScopeSelectorTest"> + <annotations> + <features value="Catalog"/> + <title value="Filtering Category Products using scope selector"/> + <description value="Filtering Category Products using scope selector"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-78408"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website, Sore adn Store View--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="adminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="adminCreateStoreView"> + <argument name="storeGroup" value="secondStoreGroup"/> + <argument name="customStore" value="secondStore"/> + </actionGroup> + + <!--Create Simple Product and Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct0"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Set filter to product name and product0 not assigned to any website--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct0.name$$')}}" + stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsitesSection"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenWebsiteSection"/> + <waitForPageLoad stepKey="waitForToOpenedWebsiteSection"/> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage"/> + + <!-- Set filter to product name and product2 in website 2 only --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct2.name$$')}}" + stepKey="clickOpenProductForEdit1"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen1"/> + + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct1"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage1"/> + + <!-- Set filter to product name and product12 assigned to both websites 1 and 2 --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad1"/> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createProduct12.name$$')}}" + stepKey="clickOpenProductForEdit2"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen2"/> + + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites1"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." + stepKey="seeSuccessMessage2"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductFilters"/> + <deleteData createDataKey="createProduct0" stepKey="deleteProduct"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct3"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Step 1-2: Open Category page and Set scope selector to All Store Views--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="clickCategoryName"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="seeProductName"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName3"/> + + <!-- Step 3: Set scope selector to Website1( Storeview for the Website 1) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewDropdownToggle}}" + stepKey="clickStoresList"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewOption('Default Store View')}}" + stepKey="clickStoreView"/> + <waitForElementVisible selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="waitForPopup1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" stepKey="clickActionAccept"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad2"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName4"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName5"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="dontSeeProductName1"/> + + <!-- Step 4: Set scope selector to Website2 ( StopreView for Website 2) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewDropdownToggle}}" + stepKey="clickStoresList1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad3"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewOption(secondStore.name)}}" + stepKey="clickStoreView1"/> + <waitForElementVisible selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="waitForPopup2"/> + <click selector="{{AdminCategoryMainActionsSection.categoryStoreViewModalAccept}}" + stepKey="clickActionAccept1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad4"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName6"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName7"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName2"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="dontSeeProductName3"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml new file mode 100644 index 0000000000000..197de39ead756 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminRemoveImageAffectsAllScopesTest"> + <annotations> + <features value="Catalog"/> + <stories value="MAGETWO-73449: Changes in default scope not effect product images in other scopes"/> + <title value="Effect of product images changes in default scope to other scopes"/> + <description value="Product image should be deleted from all scopes"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95344"/> + <group value="catalog"/> + </annotations> + <before> + <!-- login to admin, create default category and product --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create first custom website, store, store view --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{CustomWebSite.name}}"/> + <argument name="websiteCode" value="{{CustomWebSite.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createStore"> + <argument name="website" value="{{CustomWebSite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="storeGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!-- Create second custom website, store, store view --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{CustomWebSite.name}}"/> + </actionGroup> + + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + + <deleteData createDataKey="createCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteFirstProduct"/> + + <!-- Open product index page, clear filters and change gridview to default view --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + <actionGroup ref="resetAdminDataGridToDefaultView" stepKey="resetAdminDataGridToDefaultView"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Add image to product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProduct"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Add second image to product --> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!--"Product in Websites": select both Websites--> + <actionGroup ref="ProductSetWebsite" stepKey="productSetWebsite1"> + <argument name="website" value="CustomWebSite"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="productSetWebsite2"> + <argument name="website" value="SecondWebsite"/> + </actionGroup> + + <!--Open created product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!--Delete Image 1--> + <actionGroup ref="RemoveProductImage" stepKey="removeProductImage"/> + + <!--Click "Save" in the upper right corner--> + <actionGroup ref="saveProductForm" stepKey="saveProductFormAfterRemove"/> + + <!--Switch to "Store view 1"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectStoreView"> + <argument name="storeViewName" value="customStore"/> + </actionGroup> + + <!-- Assert product first image not in admin product form --> + <actionGroup ref="AssertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInAdminProductPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!--Switch to "Store view 2"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> + <argument name="storeViewName" value="SecondStoreUnique"/> + </actionGroup> + + <!-- Verify that Image 1 is deleted from the Second Store View list --> + <actionGroup ref="AssertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInSecondStoreViewPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index e92c47d2a3f48..31ed251a34795 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -120,6 +120,7 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearch"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> @@ -176,4 +177,4 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml index 714ea833d5b0b..b5f7ddd8f00cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml @@ -65,6 +65,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php index bd8871c21de9e..4435446f77bfb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php @@ -69,10 +69,17 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase /** * Allowed extensions * - * @var string + * @var array */ private $allowedExtensions; + /** + * Allowed mime types + * + * @var array + */ + private $allowedMimeTypes; + protected function setUp() { $this->coreFileStorageDatabaseMock = $this->createMock( @@ -97,6 +104,7 @@ protected function setUp() $this->baseTmpPath = 'base/tmp/'; $this->basePath = 'base/real/'; $this->allowedExtensions = ['.jpg']; + $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; $this->imageUploader = new \Magento\Catalog\Model\ImageUploader( @@ -107,7 +115,8 @@ protected function setUp() $this->loggerMock, $this->baseTmpPath, $this->basePath, - $this->allowedExtensions + $this->allowedExtensions, + $this->allowedMimeTypes ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php index 627aa1848506e..f5ed96882b981 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ImageTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -81,6 +82,11 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $cacheManager; + /** + * @var ScopeConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -89,7 +95,6 @@ protected function setUp() ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->context->expects($this->any())->method('getCacheManager')->will($this->returnValue($this->cacheManager)); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManager::class) ->disableOriginalConstructor() ->setMethods(['getStore', 'getWebsite'])->getMock(); @@ -98,24 +103,20 @@ protected function setUp() $store->expects($this->any())->method('getId')->will($this->returnValue(1)); $store->expects($this->any())->method('getBaseUrl')->will($this->returnValue('http://magento.com/media/')); $this->storeManager->expects($this->any())->method('getStore')->will($this->returnValue($store)); - $this->config = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) ->setMethods(['getBaseMediaPath'])->disableOriginalConstructor()->getMock(); $this->config->expects($this->any())->method('getBaseMediaPath')->will($this->returnValue('catalog/product')); $this->coreFileHelper = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) ->setMethods(['saveFile', 'deleteFolder'])->disableOriginalConstructor()->getMock(); - $this->mediaDirectory = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Write::class) ->disableOriginalConstructor() ->setMethods(['create', 'isFile', 'isExist', 'getAbsolutePath']) ->getMock(); - $this->filesystem = $this->createMock(\Magento\Framework\Filesystem::class); $this->filesystem->expects($this->once())->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) ->will($this->returnValue($this->mediaDirectory)); $this->factory = $this->createMock(\Magento\Framework\Image\Factory::class); - $this->viewAssetImageFactory = $this->getMockBuilder(ImageFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) @@ -141,6 +142,10 @@ function ($value) { return json_decode($value, true); } ); + $this->scopeConfig = $this->getMockBuilder(ScopeConfig::class) + ->setMethods(['getValue'])->getMockForAbstractClass(); + $this->scopeConfig->expects($this->any())->method('getValue') + ->with('system/upload_configuration/jpeg_quality')->willReturn(80); $this->image = $objectManager->getObject( \Magento\Catalog\Model\Product\Image::class, @@ -153,7 +158,8 @@ function ($value) { 'imageFactory' => $this->factory, 'viewAssetImageFactory' => $this->viewAssetImageFactory, 'viewAssetPlaceholderFactory' => $this->viewAssetPlaceholderFactory, - 'serializer' => $this->serializer + 'serializer' => $this->serializer, + 'scopeConfig' => $this->scopeConfig ] ); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index c56d3d2d7d354..6a6d74c6cb9b3 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -32,6 +32,7 @@ use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper; use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper; use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; +use \Magento\Catalog\Model\Product\Type as ProductType; /** * Class Eav @@ -398,7 +399,7 @@ public function modifyData(array $data) foreach ($attributes as $attribute) { if (null !== ($attributeValue = $this->setupAttributeData($attribute))) { - if ($attribute->getFrontendInput() === 'price' && is_scalar($attributeValue)) { + if ($this->isPriceAttribute($attribute, $attributeValue)) { $attributeValue = $this->formatPrice($attributeValue); } $data[$productId][self::DATA_SOURCE_DEFAULT][$attribute->getAttributeCode()] = $attributeValue; @@ -409,6 +410,32 @@ public function modifyData(array $data) return $data; } + /** + * Obtain if given attribute is a price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @param string|integer $attributeValue + * @return bool + */ + private function isPriceAttribute(ProductAttributeInterface $attribute, $attributeValue) + { + return $attribute->getFrontendInput() === 'price' + && is_scalar($attributeValue) + && !$this->isBundleSpecialPrice($attribute); + } + + /** + * Obtain if current product is bundle and given attribute is special_price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return bool + */ + private function isBundleSpecialPrice(ProductAttributeInterface $attribute) + { + return $this->locator->getProduct()->getTypeId() === ProductType::TYPE_BUNDLE + && $attribute->getAttributeCode() === ProductAttributeInterface::CODE_SPECIAL_PRICE; + } + /** * Resolve data persistence * diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index ec3ef58ded569..e598773ac368d 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -373,7 +373,8 @@ protected function customizeNameListeners(array $meta) $skuPath . static::META_CONFIG_PATH, $meta, [ - 'autoImportIfEmpty' => true + 'autoImportIfEmpty' => true, + 'validation' => ['no-marginal-whitespace' => true] ] ); diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index 9739ee28a6dae..dd3008a4a98a4 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -193,4 +193,16 @@ <type name="Magento\Eav\Api\AttributeSetRepositoryInterface"> <plugin name="remove_products" type="Magento\Catalog\Plugin\Model\AttributeSetRepository\RemoveProducts"/> </type> + <type name="Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Attributes"> + <arguments> + <argument name="excludeFields" xsi:type="array"> + <item name="0" xsi:type="string">category_ids</item> + <item name="1" xsi:type="string">gallery</item> + <item name="2" xsi:type="string">image</item> + <item name="3" xsi:type="string">media_gallery</item> + <item name="4" xsi:type="string">quantity_and_stock_status</item> + <item name="5" xsi:type="string">tier_price</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 1f78de93b369d..7d62d46210ea2 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -83,8 +83,9 @@ <backend_model>Magento\Catalog\Model\Indexer\Product\Flat\System\Config\Mode</backend_model> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="default_sort_by" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="default_sort_by" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Product Listing Sort by</label> + <comment>Applies to category pages</comment> <source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model> </field> <field id="list_allow_all" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -185,5 +186,19 @@ </field> </group> </section> + <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> + <class>separator-top</class> + <label>System</label> + <tab>advanced</tab> + <resource>Magento_Config::config_system</resource> + <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Images Upload Configuration</label> + <field id="jpeg_quality" translate="label comment" type="text" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Quality</label> + <validate>validate-digits validate-digits-range digits-range-1-100 required-entry</validate> + <comment>Jpeg quality for resized images 1-100%.</comment> + </field> + </group> + </section> </system> </config> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index 74f7a0e082dd6..b7733a1ab0239 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -65,6 +65,9 @@ <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> </media_storage_configuration> + <upload_configuration> + <jpeg_quality>80</jpeg_quality> + </upload_configuration> </system> <design> <watermark> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index c64ed54608542..22c6495c2bc93 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -212,6 +212,12 @@ <item name="gif" xsi:type="string">gif</item> <item name="png" xsi:type="string">png</item> </argument> + <argument name="allowedMimeTypes" xsi:type="array"> + <item name="jpg" xsi:type="string">image/jpg</item> + <item name="jpeg" xsi:type="string">image/jpeg</item> + <item name="gif" xsi:type="string">image/gif</item> + <item name="png" xsi:type="string">image/png</item> + </argument> </arguments> </virtualType> <type name="Magento\Catalog\Controller\Adminhtml\Category\Image\Upload"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml index 9c568cab16d84..ed380722d8b43 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/wysiwyg/js.phtml @@ -43,6 +43,7 @@ var catalogWysiwygEditor = { if (this.modal) { this.modal.html(jQuery(data).html()); + this.modal.modal('option', 'firedElementId', elementId); } else { this.modal = jQuery(data).modal({ title: '<?= /* @escapeNotVerified */ __('WYSIWYG Editor') ?>', diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index b1af46b80552d..038bea86e7d4e 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml @@ -21,7 +21,7 @@ $label = $block->getChildData($alias, 'title'); ?> <div class="data item title" - aria-labeledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" + aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> <a class="data switch" tabindex="-1" diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 86196cbe652a2..bf46c0efb2a74 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -352,7 +352,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig * @param \Magento\Catalog\Model\ResourceModel\ProductFactory $productFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFactory @@ -361,9 +361,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeColFactory * @param Product\Type\Factory $_typeFactory - * @param \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider - * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer + * @param ProductEntity\LinkTypeProvider $linkTypeProvider + * @param RowCustomizerInterface $rowCustomizer * @param array $dateAttrCodes + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -517,10 +518,13 @@ protected function getMediaGallery(array $productIds) if (empty($productIds)) { return []; } + + $productEntityJoinField = $this->getProductEntityLinkField(); + $select = $this->_connection->select()->from( ['mgvte' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value_to_entity')], [ - "mgvte.{$this->getProductEntityLinkField()}", + "mgvte.$productEntityJoinField", 'mgvte.value_id' ] )->joinLeft( @@ -532,7 +536,7 @@ protected function getMediaGallery(array $productIds) ] )->joinLeft( ['mgv' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value')], - '(mg.value_id = mgv.value_id)', + "(mg.value_id = mgv.value_id) and (mgvte.$productEntityJoinField = mgv.$productEntityJoinField)", [ 'mgv.label', 'mgv.position', @@ -540,14 +544,14 @@ protected function getMediaGallery(array $productIds) 'mgv.store_id' ] )->where( - "mgvte.{$this->getProductEntityLinkField()} IN (?)", + "mgvte.$productEntityJoinField IN (?)", $productIds ); $rowMediaGallery = []; $stmt = $this->_connection->query($select); while ($mediaRow = $stmt->fetch()) { - $rowMediaGallery[$mediaRow[$this->getProductEntityLinkField()]][] = [ + $rowMediaGallery[$mediaRow[$productEntityJoinField]][] = [ '_media_attribute_id' => $mediaRow['attribute_id'], '_media_image' => $mediaRow['filename'], '_media_label' => $mediaRow['label'], @@ -689,6 +693,8 @@ protected function updateDataWithCategoryColumns(&$dataRow, &$rowCategories, $pr } /** + * Get header columns + * * {@inheritdoc} */ public function _getHeaderColumns() @@ -748,6 +754,8 @@ protected function _getExportMainAttrCodes() } /** + * Get entity collection + * * {@inheritdoc} */ protected function _getEntityCollection($resetCollection = false) @@ -818,9 +826,8 @@ protected function paginateCollection($page, $pageSize) } /** - * Export process - * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function export() { @@ -854,7 +861,11 @@ public function export() } /** - * {@inheritdoc} + * Apply filter to collection and add not skipped attributes to select. + * + * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection + * * @since 100.2.0 */ protected function _prepareEntityCollection(\Magento\Eav\Model\Entity\Collection\AbstractCollection $collection) @@ -916,11 +927,10 @@ protected function getExportData() } /** - * Load products' data from the collection - * and filter it (if needed). + * Load products' data from the collection and filter it (if needed). * - * @return array Keys are product IDs, values arrays with keys as store IDs - * and values as store-specific versions of Product entity. + * @return array Keys are product IDs, values arrays with keys as store ID + * and values as store-specific versions of Product entity. */ protected function loadCollection(): array { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 19e33ee267da7..3cf167e9e37e7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1574,8 +1574,14 @@ protected function _saveProducts() $rowScope = $this->getRowScope($rowData); $urlKey = $this->getUrlKey($rowData); - if (!empty($urlKey)) { + if (!empty($rowData[self::URL_KEY])) { + // If url_key column and its value were in the CSV file $rowData[self::URL_KEY] = $urlKey; + } else if ($this->isNeedToChangeUrlKey($rowData)) { + // If url_key column was empty or even not declared in the CSV file but by the rules it is need to + // be setteed. In case when url_key is generating from name column we have to ensure that the bunch + // of products will pass for the event with url_key column. + $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; } $rowSku = $rowData[self::COL_SKU]; @@ -2475,17 +2481,17 @@ public function validateRow(array $rowData, $rowNum) } /** + * Check if need to validate url key. + * * @param array $rowData * @return bool */ private function isNeedToValidateUrlKey($rowData) { - $urlKey = $this->getUrlKey($rowData); - - return (!empty($urlKey)) + return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) && (empty($rowData[self::COL_VISIBILITY]) - || $rowData[self::COL_VISIBILITY] - !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); + || $rowData[self::COL_VISIBILITY] + !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); } /** @@ -2785,8 +2791,11 @@ protected function getProductUrlSuffix($storeId = null) } /** + * Retrieve url key from provided row data. + * * @param array $rowData * @return string + * * @since 100.0.3 */ protected function getUrlKey($rowData) @@ -2794,14 +2803,8 @@ protected function getUrlKey($rowData) if (!empty($rowData[self::URL_KEY])) { return $this->productUrl->formatUrlKey($rowData[self::URL_KEY]); } - - /** - * If the product exists, assume it already has a URL Key and even - * if a name is provided in the import data, it should not be used - * to overwrite that existing URL Key the product already has. - */ - $isSkuExist = $this->isSkuExist($rowData[self::COL_SKU]); - if (!$isSkuExist && !empty($rowData[self::COL_NAME])) { + + if (!empty($rowData[self::COL_NAME])) { return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); } @@ -2820,6 +2823,26 @@ protected function getResource() return $this->_resource; } + /** + * Whether a url key is needed to be change. + * + * @param array $rowData + * @return bool + */ + private function isNeedToChangeUrlKey(array $rowData): bool + { + $urlKey = $this->getUrlKey($rowData); + $productExists = $this->isSkuExist($rowData[self::COL_SKU]); + $markedToEraseUrlKey = isset($rowData[self::URL_KEY]); + // The product isn't new and the url key index wasn't marked for change. + if (!$urlKey && $productExists && !$markedToEraseUrlKey) { + // Seems there is no need to change the url key + return false; + } + + return true; + } + /** * Get product entity link field * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index d2d8770f5bc83..f9872c0b2acd6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -66,6 +66,8 @@ public function __construct( } /** + * Initialize categories to be processed + * * @return $this */ protected function initCategories() @@ -112,6 +114,9 @@ protected function createCategory($name, $parentId) if (!($parentCategory = $this->getCategoryById($parentId))) { $parentCategory = $this->categoryFactory->create()->load($parentId); } + + // Set StoreId to 0 to generate URL Keys global and prevent generating url rewrites just for default website + $category->setStoreId(0); $category->setPath($parentCategory->getPath()); $category->setParentId($parentId); $category->setName($name); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 60bfdd56a718e..793ad935363d3 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\Framework\Validator\AbstractValidator; +use Magento\Catalog\Model\Product\Attribute\Backend\Sku; /** * Class Validator @@ -69,6 +70,8 @@ protected function textValidation($attrCode, $type) $val = $this->string->cleanString($this->_rowData[$attrCode]); if ($type == 'text') { $valid = $this->string->strlen($val) < Product::DB_MAX_TEXT_LENGTH; + } else if ($attrCode == Product::COL_SKU) { + $valid = $this->string->strlen($val) <= SKU::SKU_MAX_LENGTH; } else { $valid = $this->string->strlen($val) < Product::DB_MAX_VARCHAR_LENGTH; } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index 13569632ab5d8..72e082392059e 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- action group to create a new catalog price rule giving a catalogRule entity --> <actionGroup name="CreateCatalogPriceRule"> <arguments> @@ -56,4 +56,16 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear2"/> <see userInput="You deleted the rule." stepKey="verifyRuleIsDeleted"/> </actionGroup> + <!--Add Catalog Rule Condition With product SKU--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsSKU" extends="CreateCatalogPriceRule"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <click selector="{{AdminCatalogPriceRuleSection.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.newCondition}}" after="openConditionsTab" stepKey="addNewCondition"/> + <selectOption selector="{{AdminCatalogPriceRuleConditionsSection.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|sku" after="addNewCondition" stepKey="selectTypeCondition"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.targetEllipsis('1')}}" after="selectTypeCondition" stepKey="clickEllipsis"/> + <fillField selector="{{AdminCatalogPriceRuleConditionsSection.targetInput('1', '1')}}" userInput="{{productSku}}" after="clickEllipsis" stepKey="fillProductSku"/> + <click selector="{{AdminCatalogPriceRuleConditionsSection.applyButton('1', '1')}}" after="fillProductSku" stepKey="clickApply"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index 1cfd8446dbe50..534a2c8cc6de2 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="CustomCatalogRule" type="catalogRule"> <data key="name" unique="suffix">CatalogPriceRule</data> <data key="description">Catalog Price Rule Description</data> @@ -21,4 +21,17 @@ <data key="simple_action">by_percent</data> <data key="discount_amount">10</data> </entity> + <entity name="CatalogRule96PercentDiscount" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="groups"> + <item>NOT LOGGED IN</item> + </array> + <array key="websites"> + <item>Main Website</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">96</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml new file mode 100644 index 0000000000000..f522fdaa03f89 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleConditionsSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogPriceRuleConditionsSection"> + <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child" timeout="30"/> + <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true" timeout="30"/> + <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true" timeout="30"/> + <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> + <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml index fc502caf2d8e6..5224147c51804 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleGridSection.xml @@ -6,9 +6,10 @@ */ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCatalogPriceRuleGridSection"> <element name="filterByRuleName" type="input" selector="#promo_catalog_grid_filter_name"/> <element name="attribute" type="text" selector="//td[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="applyRulesButton" type="button" selector="#apply_rules" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml index ca1f499db64fe..bbf5ed6053cd2 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminCatalogPriceRuleSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCatalogPriceRuleSection"> <element name="saveAndApply" type="button" selector="#save_and_apply" timeout="30"/> <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 381fb573b21a1..286b6427c8132 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -316,11 +316,20 @@ public function getSearchableAttributes($backendType = null) /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ $attributes = $productAttributes->getItems(); + /** + * @deprecated Event argument catelogsearch_searchable_attributes_load_after. + * @see catalogsearch_searchable_attributes_load_after instead. + */ $this->eventManager->dispatch( 'catelogsearch_searchable_attributes_load_after', ['engine' => $this->engine, 'attributes' => $attributes] ); + $this->eventManager->dispatch( + 'catalogsearch_searchable_attributes_load_after', + ['engine' => $this->engine, 'attributes' => $attributes] + ); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index c4d67f447e2cf..6eda8dd0b61ee 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -6,11 +6,15 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\UrlRewrite\Model\UrlPersistInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\Framework\Event\ObserverInterface; +/** + * Class ProductProcessUrlRewriteSavingObserver + */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** @@ -23,22 +27,33 @@ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface */ private $urlPersist; + /** + * @var ProductUrlPathGenerator + */ + private $productUrlPathGenerator; + /** * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $productUrlPathGenerator */ public function __construct( ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $productUrlPathGenerator = null ) { $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; + $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() + ->get(ProductUrlPathGenerator::class); } /** * Generate urls for UrlRewrite and save it in storage + * * @param \Magento\Framework\Event\Observer $observer * @return void + * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -51,6 +66,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) || $product->dataHasChangedFor('visibility') ) { if ($product->isVisibleInSiteVisibility()) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); } } diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index d412559390cee..cb462ada0fc91 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -196,7 +196,7 @@ public function getProductPriceHtml( ? $arguments['display_minimal_price'] : true; - /** @var \Magento\Framework\Pricing\Render $priceRender */ + /** @var \Magento\Framework\Pricing\Render $priceRender */ $priceRender = $this->getLayout()->getBlock('product.price.render.default'); $price = ''; @@ -338,7 +338,7 @@ public function getPagerHtml() if (!$this->pager) { $this->pager = $this->getLayout()->createBlock( \Magento\Catalog\Block\Product\Widget\Html\Pager::class, - 'widget.products.list.pager' + $this->getWidgetPagerBlockName() ); $this->pager->setUseContainer(true) @@ -398,4 +398,19 @@ private function getPriceCurrency() } return $this->priceCurrency; } + + /** + * @return string + */ + private function getWidgetPagerBlockName() + { + $pageName = $this->getData('page_var_name'); + $pagerBlockName = 'widget.products.list.pager'; + + if (!$pageName) { + return $pagerBlockName; + } + + return $pagerBlockName . '.' . $pageName; + } } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 3d6b0aa0cdc12..eca747f9c042f 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -114,9 +114,8 @@ public function savePaymentInformation( $quote->setDataChanges(true); $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { - $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); - $shippingCarrier = array_shift($shippingDataArray); - $shippingAddress->setLimitCarrier($shippingCarrier); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); } } $this->paymentMethodManagement->set($cartId, $paymentMethod); diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 13f7c00deaced..db747dbdba657 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -59,4 +59,11 @@ <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="openShippingDetails"/> <see selector="{{StorefrontCheckoutCartSummarySection.countryParameterized('placeNumber')}}" userInput="{{country}}" stepKey="seeCountry"/> </actionGroup> + + <!--Click Place Order button--> + <actionGroup name="ClickPlaceOrderActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index afe37a10e6634..11cd095334113 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Guest checkout filling billing section --> <actionGroup name="GuestCheckoutFillNewBillingAddressActionGroup"> <arguments> @@ -25,4 +25,35 @@ <fillField selector="{{CheckoutPaymentSection.guestTelephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> </actionGroup> + <actionGroup name="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <arguments> + <argument name="customerAddress"/> + <!-- the classPrefix argument is to specifically select the inputs of the correct form + this is to prevent having 3 action groups doing essentially the same thing --> + <argument name="classPrefix" type="string" defaultValue=""/> + </arguments> + <fillField stepKey="fillFirstName" selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" userInput="{{customerAddress.firstname}}"/> + <fillField stepKey="fillLastName" selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" userInput="{{customerAddress.lastname}}"/> + <fillField stepKey="fillPhoneNumber" selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" userInput="{{customerAddress.telephone}}"/> + <fillField stepKey="fillStreetAddress1" selector="{{classPrefix}} {{CheckoutShippingSection.street}}" userInput="{{customerAddress.street[0]}}"/> + <fillField stepKey="fillCityName" selector="{{classPrefix}} {{CheckoutShippingSection.city}}" userInput="{{customerAddress.city}}"/> + <selectOption stepKey="selectState" selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="{{customerAddress.state}}"/> + <fillField stepKey="fillZip" selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" userInput="{{customerAddress.postcode}}"/> + <selectOption stepKey="selectCounty" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{customerAddress.country_id}}"/> + <waitForPageLoad stepKey="waitForFormUpdate2"/> + </actionGroup> + + <actionGroup name="clearCheckoutAddressPopupFieldsActionGroup"> + <arguments> + <argument name="classPrefix" type="string" defaultValue=""/> + </arguments> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" stepKey="clearFieldLastName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress1"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml index 9e66e742229bd..8ff84e7a436e5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml @@ -7,12 +7,14 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Guest checkout filling shipping section --> <actionGroup name="GuestCheckoutFillingShippingSectionActionGroup"> <arguments> - <argument name="customerVar"/> - <argument name="customerAddressVar"/> + <argument name="customerVar" defaultValue="CustomerEntityOne"/> + <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> </arguments> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> @@ -23,11 +25,11 @@ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> - <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml index f1d792a638746..0c6a1682a7719 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml @@ -7,7 +7,7 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="CheckoutPage" url="/checkout" area="storefront" module="Magento_Checkout"> <section name="CheckoutShippingSection"/> <section name="CheckoutShippingMethodsSection"/> @@ -15,5 +15,6 @@ <section name="CheckoutSuccessMainSection"/> <section name="CheckoutPaymentSection"/> <section name="CheckoutGuestShippingInfoSection"/> + <section name="CheckoutAddressPopupSection"/> </page> </pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml new file mode 100644 index 0000000000000..9e8adab362a88 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutAddressPopupSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutAddressPopupSection"> + <element name="newAddressModalPopup" type="block" selector=".modal-popup.modal-slide._inner-scroll"/> + <element name="closeAddressModalPopup" type="button" selector=".action-hide-popup"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index f9e27ef36c715..c73001cfe6832 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutPaymentSection"> <element name="isPaymentSection" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Review & Payments')]]"/> <element name="availablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div:nth-child(2)>div.payment-method-title.field.choice"/> @@ -25,12 +25,13 @@ <element name="cartItems" type="text" selector=".minicart-items"/> <element name="billingAddress" type="text" selector="div.billing-address-details"/> <element name="placeOrder" type="button" selector="button.action.primary.checkout" timeout="30"/> - <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> - <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> + <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> + <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> + <element name="productItemPriceByName" type="text" selector="//div[@class='product-item-details'][contains(., '{{ProductName}}')]//span[@class='price']" parameterized="true"/> <element name="productOptionsActiveByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true" /> <element name="productOptionsByProductItemName" type="text" selector="//div[@class='product-item-details']//strong[@class='product-item-name'][text()='{{var1}}']//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true" /> <element name="cartItemsArea" type="button" selector=".items-in-cart"/> - <element name="cartItemsAreaActive" type="textarea" selector=".items-in-cart.active"/> + <element name="cartItemsAreaActive" type="textarea" selector=".items-in-cart.active" timeout="30"/> <element name="paymentSectionTitle" type="text" selector="#checkout-payment-method-load .step-title" /> <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> <element name="checkMoneyOrderPayment" type="radio" selector="#checkmo.radio" timeout="30"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index f36d2a9b667cd..2a7437c44eccf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingSection"> <element name="isShippingStep" type="text" selector="//*[@class='opc-progress-bar']/li[contains(@class, '_active') and span[contains(.,'Shipping')]]"/> <element name="email" type="input" selector="#customer-email"/> @@ -24,7 +24,7 @@ <element name="newAddressButton" type="button" selector="#checkout-step-shipping button"/> <element name="next" type="button" selector="[data-role='opc-continue']"/> <element name="stateInput" type="input" selector="input[name=region]"/> - <element name="newAdress" type="button" selector="button.action.action-show-popup"/> + <element name="newAdress" type="button" selector="button.action.action-show-popup" timeout="30"/> <element name="addStreet" type="input" selector="#shipping-new-address-form input[name='street[0]']"/> <element name="addCity" type="input" selector="#shipping-new-address-form input[name='city']"/> <element name="addState" type="select" selector="#shipping-new-address-form select[name='region_id']"/> @@ -32,5 +32,6 @@ <element name="addTelephone" type="input" selector="#shipping-new-address-form input[name='telephone']"/> <element name="addcCountry" type="select" selector="#shipping-new-address-form select[name='country_id']"/> <element name="addSaveButton" type="button" selector=".action.primary.action-save-address"/> + <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml index 332648751e89f..a7c5fb484b2bf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCartSummarySection.xml @@ -16,6 +16,7 @@ <element name="postcode" type="text" selector="#block-summary input[name='postcode']" timeout="30"/> <element name="estimateShippingAndTax" type="text" selector="#block-shipping-heading" timeout="5"/> <element name="flatRateShippingMethod" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="itemDiscount" type="text" selector="tr[class='totals'] td.amount > span"/> <element name="countryParameterized" type="select" selector="select[name='country_id'] > option:nth-child({{var}})" timeout="10" parameterized="true"/> <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> <element name="blockSummary" type="button" selector="#block-summary"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml index ad26118ac2f06..7dba25812f0dd 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithTaxTest.xml @@ -70,7 +70,6 @@ <waitForPageLoad stepKey="waitForPage"/> <!-- Step 7: Click New Address button --> <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="clickAddNewAddress"/> - <waitForPageLoad stepKey="waitForPagePopup"/> <!-- Step 8: Fill form with valid data and set: California --> <!-- Step 9: Click Save Address --> <actionGroup ref="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingAddress"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml new file mode 100644 index 0000000000000..c80e284633f12 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Customer Checkout"/> + <description value="Customer can place order with new addresses."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77181"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!--Go to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> + <argument name="customerAddress" value="UK_Default_Address"/> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Click "Save Addresses" --> + <click selector="{{CheckoutShippingSection.addSaveButton}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <dontSeeElement selector="{{CheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + + <!--Select Shipping Rate "Flat Rate"--> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + + <!--Click "Edit" for the new address and clear required fields--> + <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> + <actionGroup ref="clearCheckoutAddressPopupFieldsActionGroup" stepKey="clearRequiredFields"> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Close Popup and click next--> + <click selector="{{CheckoutAddressPopupSection.closeAddressModalPopup}}" stepKey="closePopup"/> + <waitForPageLoad stepKey="waitForPopupClosed"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + + <!--Refresh Page and Place Order--> + <reloadPage stepKey="reloadPage"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="grabOrderNumber"/> + + <!--Verify New addresses in Customer's Address Book--> + <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToCustomerAddressBook"/> + <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddresses"/> + <!--Order review page has address that was created during checkout--> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> + <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> + <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index bb6c19ac7fe47..dff957457da95 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -63,6 +63,7 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml new file mode 100644 index 0000000000000..cda8039e505a8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdatePriceInShoppingCartAfterProductSaveTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Update price in shopping cart after product save"/> + <description value="Price in shopping cart should be updated after product save with changed price"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-77832"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"> + <field key="price">100</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setCustomerDataLifetime"> + <argument name="minutes" value="1"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindex"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check price--> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice"/> + + <!--Edit product price via admin panel--> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <fillField userInput="120" selector="{{AdminProductFormSection.productPrice}}" stepKey="setNewPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <closeTab stepKey="closeTab"/> + + <!--Check price--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index b40b2b244ac4c..6ecd759b8b826 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -168,9 +168,11 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $billingAddressId = 1; $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); $quoteShippingAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['setLimitCarrier', 'getShippingMethod'] + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] ); $this->cartRepositoryMock->expects($this->any())->method('getActive')->with($cartId)->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($quoteBillingAddress); @@ -179,6 +181,7 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $quoteMock->expects($this->once())->method('removeAddress')->with($billingAddressId); $quoteMock->expects($this->once())->method('setBillingAddress')->with($billingAddressMock); $quoteMock->expects($this->once())->method('setDataChanges')->willReturnSelf(); + $quoteShippingAddress->expects($this->any())->method('getShippingRateByCode')->willReturn($shippingRate); $quoteShippingAddress->expects($this->any())->method('getShippingMethod')->willReturn('flatrate_flatrate'); $quoteShippingAddress->expects($this->once())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); } diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 71b1392d5391f..1005c11e44d95 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -24,9 +24,9 @@ <?php endif ?> <table id="shopping-cart-table" class="cart items data table" - data-mage-init='{"shoppingCart":{"emptyCartButton": "action.clear", + data-mage-init='{"shoppingCart":{"emptyCartButton": ".action.clear", "updateCartActionContainer": "#update_cart_action_container"}}'> - <caption role="heading" aria-level="2" class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> + <caption class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> <thead> <tr> <th class="col item" scope="col"><span><?= /* @escapeNotVerified */ __('Item') ?></span></th> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index 0567c61f0db60..c96df9cdd3195 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -111,7 +111,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima </td> </tr> <tr class="item-actions"> - <td colspan="100"> + <td colspan="4"> <div class="actions-toolbar"> <?= /* @escapeNotVerified */ $block->getActions($_item) ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 76e3d911e7d3f..54e496131972e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -14,55 +14,71 @@ define([ 'use strict'; var rateProcessors = [], - totalsProcessors = []; + totalsProcessors = [], - quote.shippingAddress.subscribe(function () { - var type = quote.shippingAddress().getType(); + /** + * Estimate totals for shipping address and update shipping rates. + */ + estimateTotalsAndUpdateRates = function () { + var type = quote.shippingAddress().getType(); - if ( - quote.isVirtual() || - window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 - ) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.shippingAddress()) : - totalsProcessors['default'].estimateTotals(quote.shippingAddress()); - } else { - // check if user data not changed -> load rates from cache - if (!cartCache.isChanged('address', quote.shippingAddress()) && - !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && - cartCache.get('rates') + if ( + quote.isVirtual() || + window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 ) { - shippingService.setShippingRates(cartCache.get('rates')); + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.shippingAddress()) : + totalsProcessors['default'].estimateTotals(quote.shippingAddress()); + } else { + // check if user data not changed -> load rates from cache + if (!cartCache.isChanged('address', quote.shippingAddress()) && + !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + cartCache.get('rates') + ) { + shippingService.setShippingRates(cartCache.get('rates')); - return; + return; + } + + // update rates list when estimated address was set + rateProcessors['default'] = defaultProcessor; + rateProcessors[type] ? + rateProcessors[type].getRates(quote.shippingAddress()) : + rateProcessors['default'].getRates(quote.shippingAddress()); + + // save rates to cache after load + shippingService.getShippingRates().subscribe(function (rates) { + cartCache.set('rates', rates); + }); } + }, - // update rates list when estimated address was set - rateProcessors['default'] = defaultProcessor; - rateProcessors[type] ? - rateProcessors[type].getRates(quote.shippingAddress()) : - rateProcessors['default'].getRates(quote.shippingAddress()); + /** + * Estimate totals for shipping address. + */ + estimateTotalsShipping = function () { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + }, - // save rates to cache after load - shippingService.getShippingRates().subscribe(function (rates) { - cartCache.set('rates', rates); - }); - } - }); - quote.shippingMethod.subscribe(function () { - totalsDefaultProvider.estimateTotals(quote.shippingAddress()); - }); - quote.billingAddress.subscribe(function () { - var type = quote.billingAddress().getType(); + /** + * Estimate totals for billing address. + */ + estimateTotalsBilling = function () { + var type = quote.billingAddress().getType(); + + if (quote.isVirtual()) { + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.billingAddress()) : + totalsProcessors['default'].estimateTotals(quote.billingAddress()); + } + }; - if (quote.isVirtual()) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.billingAddress()) : - totalsProcessors['default'].estimateTotals(quote.billingAddress()); - } - }); + quote.shippingAddress.subscribe(estimateTotalsAndUpdateRates); + quote.shippingMethod.subscribe(estimateTotalsShipping); + quote.billingAddress.subscribe(estimateTotalsBilling); + customerData.get('cart').subscribe(estimateTotalsShipping); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 73f4df567903c..28e04699f8daf 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -218,16 +218,31 @@ define([ * Apply resolved billing address to quote */ applyBillingAddress: function () { - var shippingAddress; + var shippingAddress, + isBillingAddressInitialized; if (quote.billingAddress()) { selectBillingAddress(quote.billingAddress()); return; } + + if (quote.isVirtual()) { + isBillingAddressInitialized = addressList.some(function (addrs) { + if (addrs.isDefaultBilling()) { + selectBillingAddress(addrs); + + return true; + } + + return false; + }); + } + shippingAddress = quote.shippingAddress(); - if (shippingAddress && + if (!isBillingAddressInitialized && + shippingAddress && shippingAddress.canUseForBilling() && (shippingAddress.isDefaultShipping() || !quote.isVirtual()) ) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js index 3a6574bff8948..447d626b339bd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js @@ -35,9 +35,8 @@ define([ saveShippingInformation: function () { var payload; - if (!quote.billingAddress()) { - selectBillingAddressAction(quote.shippingAddress()); - } + /* Assign selected address every time buyer selects address*/ + selectBillingAddressAction(quote.shippingAddress()); payload = { addressInformation: { diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index cc1d960bbe44b..ea521b3a8afd4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -8,7 +8,7 @@ <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> - <text args="currentBillingAddress().city "/>, <span html="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> + <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> <text args="getCountryName(currentBillingAddress().countryId)"/><br/> <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html index 05ced7a978f82..2a5dc27328a43 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html @@ -8,7 +8,7 @@ <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> <text args="address().lastname"/> <text args="address().suffix"/><br/> <text args="_.values(address().street).join(', ')"/><br/> - <text args="address().city "/>, <span html="address().region"></span> <text args="address().postcode"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> <text args="getCountryName(address().countryId)"/><br/> <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html index 97286a28552d2..541413955cb47 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html @@ -8,7 +8,7 @@ <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> <text args="address().lastname"/> <text args="address().suffix"/><br/> <text args="_.values(address().street).join(', ')"/><br/> - <text args="address().city "/>, <span html="address().region"></span> <text args="address().postcode"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> <text args="getCountryName(address().countryId)"/><br/> <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml index 1bc8828ef6c8e..5bf66ef302f04 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml @@ -9,7 +9,11 @@ <container name="root"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content" name="wysiwyg_images.content" template="Magento_Cms::browser/content.phtml"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree" name="wysiwyg_images.tree" template="Magento_Cms::browser/tree.phtml"/> - <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"/> + <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"> + <arguments> + <argument name="image_upload_config_data" xsi:type="object">Magento\Backend\Block\DataProviders\UploadConfig</argument> + </arguments> + </block> </block> </container> </layout> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index f87dc07dc04f4..27f14434b5fcf 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -7,6 +7,14 @@ // @codingStandardsIgnoreFile /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader */ + +$resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() + ? "{action: 'resize', maxWidth: " + . $block->getImageUploadMaxWidth() + . ", maxHeight: " + . $block->getImageUploadMaxHeight() + . "}" + : "{action: 'resize'}"; ?> <div id="<?= $block->getHtmlId() ?>" class="uploader"> @@ -99,11 +107,9 @@ require([ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/, maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 - }, { - action: 'resize', - maxWidth: <?= (int) $block->getImageUploadMaxWidth() ?> , - maxHeight: <?= (int) $block->getImageUploadMaxHeight() ?> - }, { + }, + <?= $block->escapeHtml($resizeConfig) ?>, + { action: 'save' }] }); diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index c6e2412f7e58f..0472c5daa276f 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -237,13 +237,14 @@ private function getField(string $sectionId, string $groupId, string $fieldId): * Get field path * * @param Field $field + * @param string $fieldId Need for support of clone_field feature * @param array &$oldConfig Need for compatibility with _processGroup() * @param array &$extraOldGroups Need for compatibility with _processGroup() * @return string */ - private function getFieldPath(Field $field, array &$oldConfig, array &$extraOldGroups): string + private function getFieldPath(Field $field, string $fieldId, array &$oldConfig, array &$extraOldGroups): string { - $path = $field->getGroupPath() . '/' . $field->getId(); + $path = $field->getGroupPath() . '/' . $fieldId; /** * Look for custom defined field path @@ -303,7 +304,7 @@ private function getChangedPaths( if (isset($groupData['fields'])) { foreach ($groupData['fields'] as $fieldId => $fieldData) { $field = $this->getField($sectionId, $groupId, $fieldId); - $path = $this->getFieldPath($field, $oldConfig, $extraOldGroups); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); if ($this->isValueChanged($oldConfig, $path, $fieldData)) { $changedPaths[] = $path; } @@ -398,7 +399,7 @@ protected function _processGroup( $backendModel->addData($data); $this->_checkSingleStoreMode($field, $backendModel); - $path = $this->getFieldPath($field, $extraOldGroups, $oldConfig); + $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); $backendModel->setPath($path)->setValue($fieldData['value']); $inherit = !empty($fieldData['inherit']); diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..75686d23a11b9 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateApiConfigurableProductActionGroup"> + <arguments> + <argument name="productName" defaultValue="ApiConfigurableProductWithOutCategory" type="string"/> + </arguments> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"> + <field key="name">{{productName}}</field> + </createData> + + <!-- Create attribute with 2 options to be used in children products --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addAttributeToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml index 83e0aa1deaedf..a2f824dd8864e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="BaseConfigurableProduct" type="product"> <data key="sku" unique="suffix">configurable</data> <data key="type_id">configurable</data> @@ -38,4 +38,15 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ApiConfigurableProductWithOutCategory" type="product"> + <data key="sku" unique="suffix">api-configurable-product-with-out-category</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">API Configurable Product</data> + <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml index 21dcf998a6399..e98175bc8d40b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -7,11 +7,18 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="ConfigurableProductTwoOptions" type="ConfigurableProductOption"> <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> <data key="label">option</data> <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> </entity> + <entity name="ConfigurableProductThreeOptions" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml index 54d489a446fd7..e1cd70790a6b2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml @@ -7,11 +7,14 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="ValueIndex1" type="ValueIndex"> <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> </entity> <entity name="ValueIndex2" type="ValueIndex"> <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> </entity> + <entity name="ValueIndex3" type="ValueIndex"> + <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml new file mode 100644 index 0000000000000..c8ca2ced0d60f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSortingByPriceForConfigurableProductWithCatalogRuleAppliedTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View soting by price in storefront"/> + <title value="Sorting by price for Configurable with Catalog Rule applied"/> + <description value="Sort by price should be correct if the apply Catalog Rule to child product of configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76081"/> + <group value="configurable_product"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">5.00</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10.00</field> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="ProductAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">15.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">25.00</field> + </createData> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--SKU Product Attribute is enabled for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="attributeLabel" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToYes"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!--SKU Product Attribute is disable for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="attributeLabel" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToNo"> + <argument name="useForPromoRule" value="No"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Open category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct"/> + + <!--Create and apply catalog price rule--> + <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsSKU" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CatalogRule96PercentDiscount" /> + <argument name="productSku" value="$$createConfigChildProduct3.sku$$" /> + </actionGroup> + <click selector="{{AdminCatalogPriceRuleGridSection.applyRulesButton}}" stepKey="clickApplyRules"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + + <!--Reopen category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage2"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('1')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('2')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> + <see selector="{{StorefrontCategoryMainSection.categoryPageProductName('3')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> + + <!-- Delete the rule --> + <actionGroup ref="RemoveCatalogPriceRule" stepKey="deletePriceRule"> + <argument name="ruleName" value="CatalogRule96PercentDiscount.name" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 33ee30a25b012..eae9dac046b23 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -19,6 +19,7 @@ "suggest": { "magento/module-webapi": "100.2.*", "magento/module-sales": "101.0.*", + "magento/module-sales-rule": "101.0.*", "magento/module-product-video": "100.2.*", "magento/module-configurable-sample-data": "Sample Data version:100.2.*", "magento/module-product-links-sample-data": "Sample Data version:100.2.*" diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index dfbad0dd5a764..1dbb0969687d5 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -228,4 +228,11 @@ </argument> </arguments> </type> + <type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator"> + <arguments> + <argument name="productTypeChildrenValidationMap" xsi:type="array"> + <item name="configurable" xsi:type="boolean">false</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js index 558a1fdf31085..6f18798303151 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/options-updater.js @@ -61,6 +61,7 @@ define([ this.setProductOptions(cartData()); this.updateOptions(); }.bind(this)); + this.updateOptions(); }, /** diff --git a/app/code/Magento/Customer/Model/GroupManagement.php b/app/code/Magento/Customer/Model/GroupManagement.php index eb5d90f2fd7ea..48cb5d55061c5 100644 --- a/app/code/Magento/Customer/Model/GroupManagement.php +++ b/app/code/Magento/Customer/Model/GroupManagement.php @@ -15,7 +15,6 @@ use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Data\Collection; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; @@ -170,7 +169,7 @@ public function getLoggedInGroups() ->create(); $groupNameSortOrder = $this->sortOrderBuilder ->setField('customer_group_code') - ->setDirection(Collection::SORT_ORDER_ASC) + ->setAscendingDirection() ->create(); $searchCriteria = $this->searchCriteriaBuilder ->addFilters($notLoggedInFilter) diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml new file mode 100644 index 0000000000000..3fc25ecf57faa --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.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="SetCustomerDataLifetimeActionGroup"> + <arguments> + <argument name="minutes" defaultValue="60" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerConfigPage.url('#customer_online_customers-link')}}" stepKey="openCustomerConfigPage"/> + <fillField userInput="{{minutes}}" selector="{{AdminCustomerConfigSection.customerDataLifetime}}" stepKey="fillCustomerDataLifetime"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index e4420f189900e..c8ba70c7f8b50 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="CustomerAddressSimple" type="address"> <data key="id">0</data> <data key="customer_id">12</data> @@ -89,4 +89,17 @@ <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionNY</requiredEntity> </entity> + <entity name="UK_Default_Address" type="address"> + <data key="firstname">Jane</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>172, Westminster Bridge Rd</item> + </array> + <data key="city">London</data> + <data key="state"></data> + <data key="postcode">SE1 7RW</data> + <data key="country_id">GB</data> + <data key="telephone">444-44-444-44</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml new file mode 100644 index 0000000000000..d7fc22f085344 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiCustomerGroup" type="customerGroup"> + <data key="code" unique="suffix">ApiCustomerGroup</data> + <data key="tax_class_id">0</data> + <data key="tax_class_name">Retail Customer</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml new file mode 100644 index 0000000000000..a45bf1645d088 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer-group-meta.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerGroup" dataType="customerGroup" type="create" auth="adminOauth" url="/V1/customerGroups" method="POST"> + <contentType>application/json</contentType> + <object dataType="customerGroup" key="group"> + <field key="code">string</field> + <field key="tax_class_id">integer</field> + <field key="tax_class_name">string</field> + <array key="extension_attributes"> + <value>extension_attributes</value> + </array> + </object> + </operation> + <operation name="DeleteCustomerGroup" dataType="customerGroup" type="delete" auth="adminOauth" url="/V1/customerGroups/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml new file mode 100644 index 0000000000000..3cf8490ec4af1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCustomerConfigPage" url="admin/system_config/edit/section/customer/{{tabLink}}" area="admin" parameterized="true" module="Magento_Customer"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml new file mode 100644 index 0000000000000..bdbbcab67da67 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAddressesPage" url="/customer/address/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAddressesSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml new file mode 100644 index 0000000000000..33696fbd616e3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerConfigSection"> + <element name="customerDataLifetime" type="input" selector="#customer_online_customers_section_data_lifetime"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml new file mode 100644 index 0000000000000..88b46d245105f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressesSection"> + <element name="addressesList" type="text" selector=".block-addresses-list" /> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml index 01b758c9c3e3b..f6a6cb2d457e1 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -7,12 +7,14 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerOrderViewSection"> <element name="orderTitle" type="text" selector=".page-title span"/> <element name="myOrdersTable" type="text" selector="#my-orders-table"/> <element name="subtotal" type="text" selector=".subtotal .amount"/> <element name="paymentMethod" type="text" selector=".payment-method dt"/> <element name="printOrderLink" type="text" selector="a.action.print" timeout="30"/> + <element name="shippingAddress" type="text" selector=".box.box-order-shipping-address"/> + <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index e5dc187e8ab7a..8e4d69e6a270d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCreateCustomerTest"> <annotations> <features value="Customer Creation"/> @@ -19,6 +19,9 @@ <group value="customer"/> <group value="create"/> </annotations> + <before> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + </before> <after> <actionGroup ref="logout" stepKey="logout"/> </after> diff --git a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php index 36c1e761d013c..1ff1adc3dac58 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php @@ -299,8 +299,8 @@ public function validateData() $rows = []; foreach ($source as $row) { $rows[] = [ - Address::COLUMN_EMAIL => $row[Customer::COLUMN_EMAIL], - Address::COLUMN_WEBSITE => $row[Customer::COLUMN_WEBSITE] + Address::COLUMN_EMAIL => $row[Customer::COLUMN_EMAIL] ?? null, + Address::COLUMN_WEBSITE => $row[Customer::COLUMN_WEBSITE] ?? null ]; } $source->rewind(); diff --git a/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php b/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php index 25519e5c83054..a2db0f43061ea 100644 --- a/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php +++ b/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php @@ -5,7 +5,6 @@ */ namespace Magento\Developer\Console\Command; -use Magento\Framework\App\State; use Magento\Framework\Validator\Locale; use Magento\Framework\View\Asset\Repository; use Symfony\Component\Console\Command\Command; diff --git a/app/code/Magento/Downloadable/Setup/UpgradeData.php b/app/code/Magento/Downloadable/Setup/UpgradeData.php new file mode 100644 index 0000000000000..aec15ce7f9d90 --- /dev/null +++ b/app/code/Magento/Downloadable/Setup/UpgradeData.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Downloadable\Setup; + +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\UpgradeDataInterface; +use Magento\Framework\Setup\ModuleContextInterface; + +/** + * @codeCoverageIgnore + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * EAV setup factory + * + * @var EavSetupFactory + */ + private $eavSetupFactory; + + /** + * Init + * + * @param EavSetupFactory $eavSetupFactory + */ + public function __construct(EavSetupFactory $eavSetupFactory) + { + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * @inheritdoc + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '2.0.3', '<')) { + /** @var EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); + // remove default value + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'links_exist', + 'default_value', + null + ); + } + + $setup->endSetup(); + } +} diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml new file mode 100644 index 0000000000000..4f48ca7309baa --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiDownloadableLink" type="downloadable_link"> + <data key="title" unique="suffix">Api Downloadable Link</data> + <data key="price">2.00</data> + <data key="link_type">url</data> + <data key="shareable">No</data> + <data key="number_of_downloads">1000</data> + <data key="sort_order">0</data> + <data key="link_url">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> + </entity> +</entities> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml new file mode 100644 index 0000000000000..28b4aa261b941 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiDownloadableProduct" type="product"> + <data key="sku" unique="suffix">api-downloadable-product</data> + <data key="type_id">downloadable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Downloadable Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-downloadable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + <requiredEntity type="downloadable_link">apiDownloadableLink</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link-meta.xml new file mode 100644 index 0000000000000..2511244d445c1 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link-meta.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateDownloadableLink" dataType="downloadable_link" type="create" auth="adminOauth" url="/V1/products/{sku}/downloadable-links" method="POST"> + <contentType>application/json</contentType> + <object dataType="downloadable_link" key="link"> + <field key="title">string</field> + <field key="sort_order">integer</field> + <field key="is_shareable">integer</field> + <field key="price">number</field> + <field key="number_of_downloads">integer</field> + <field key="link_type">string</field> + <field key="link_file">string</field> + <field key="link_file_content">link_file_content</field> + <field key="file_data">string</field> + <field key="link_url">string</field> + <field key="sample_type">string</field> + <field key="sample_file">string</field> + <field key="sample_file_content">sample_file_content</field> + <field key="sample_url">string</field> + </object> + <field key="isGlobalScopeContent">boolean</field> + </operation> +</operations> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/link_file_content-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/link_file_content-meta.xml new file mode 100644 index 0000000000000..d5d6c16c71736 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/link_file_content-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateLinkFileContent" dataType="link_file_content" type="create"> + <field key="file_data">string</field> + <field key="name">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/sample_file_content-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/sample_file_content-meta.xml new file mode 100644 index 0000000000000..3da91807ceb48 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/sample_file_content-meta.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSampleFileContent" dataType="sample_file_content" type="create"> + <field key="file_data">string</field> + <field key="name">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Downloadable/etc/module.xml b/app/code/Magento/Downloadable/etc/module.xml index 4c4e165feb014..fb13121335730 100644 --- a/app/code/Magento/Downloadable/etc/module.xml +++ b/app/code/Magento/Downloadable/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Downloadable" setup_version="2.0.2"> + <module name="Magento_Downloadable" setup_version="2.0.3"> <sequence> <module name="Magento_Catalog"/> </sequence> diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 65b60be0d96fd..1f13fe1405273 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -392,7 +392,7 @@ public function getIsVisibleOnFront() } /** - * @return string|int|bool|float + * @return string|null * @codeCoverageIgnore */ public function getDefaultValue() diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php index 38e7b883f4ea5..e4bf1892d7222 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php @@ -203,12 +203,12 @@ public function getEntityValueId($entity) /** * Retrieve default value * - * @return mixed + * @return string */ public function getDefaultValue() { if ($this->_defaultValue === null) { - if ($this->getAttribute()->getDefaultValue()) { + if ($this->getAttribute()->getDefaultValue() !== null) { $this->_defaultValue = $this->getAttribute()->getDefaultValue(); } else { $this->_defaultValue = ""; @@ -280,7 +280,7 @@ public function afterLoad($object) public function beforeSave($object) { $attrCode = $this->getAttribute()->getAttributeCode(); - if (!$object->hasData($attrCode) && $this->getDefaultValue()) { + if (!$object->hasData($attrCode) && $this->getDefaultValue() !== '') { $object->setData($attrCode, $this->getDefaultValue()); } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 01bdddc32af90..4a620ed5f35e5 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -132,7 +132,7 @@ public function getItems($entityType, $attributeCode) */ protected function validateOption($attribute, $optionId) { - if (!$attribute->getSource()->getOptionText($optionId)) { + if ($attribute->getSource()->getOptionText($optionId) === false) { throw new NoSuchEntityException( __('Attribute %1 does not contain option with Id %2', $attribute->getAttributeCode(), $optionId) ); diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 77b4ce1c8eae8..2177686a4a6be 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -1233,7 +1233,7 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) if ($entity->getEntityTable() == \Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE && $entity->getTypeId()) { $select->where( - 'entity_type_id =?', + 't_d.entity_type_id =?', $entity->getTypeId() ); } diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 81e993fad76df..a6ecdaf24ebbb 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Email\Model; use Magento\Framework\App\Filesystem\DirectoryList; @@ -289,7 +290,7 @@ public function loadDefault($templateId) /** * trim copyright message */ - if (preg_match('/^<!--[\w\W]+?-->/m', $templateText, $matches) && strpos($matches[0], 'Copyright') > 0) { + if (preg_match('/^<!--[\w\W]+?-->/m', $templateText, $matches) && strpos($matches[0], 'Copyright') !== false) { $templateText = str_replace($matches[0], '', $templateText); } @@ -530,13 +531,13 @@ protected function cancelDesignConfig() * * @param string $templateId * @return $this - * @throws \Magento\Framework\Exception\MailException */ public function setForcedArea($templateId) { - if (!isset($this->area)) { + if ($this->area === null) { $this->area = $this->emailConfig->getTemplateArea($templateId); } + return $this; } @@ -604,7 +605,9 @@ public function getDesignConfig() public function setDesignConfig(array $config) { if (!isset($config['area']) || !isset($config['store'])) { - throw new LocalizedException(__('Design config must have area and store.')); + throw new LocalizedException( + __('The design config needs an area and a store. Verify that both are set and try again.') + ); } $this->getDesignConfig()->setData($config); return $this; diff --git a/app/code/Magento/Email/Model/Transport.php b/app/code/Magento/Email/Model/Transport.php index 7e966a9a5a28d..dc7824a228e34 100644 --- a/app/code/Magento/Email/Model/Transport.php +++ b/app/code/Magento/Email/Model/Transport.php @@ -7,13 +7,16 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\MailException; -use Magento\Framework\Mail\MessageInterface; +use Magento\Framework\Mail\Message; use Magento\Framework\Mail\TransportInterface; +use Magento\Framework\Phrase; use Magento\Store\Model\ScopeInterface; +use Zend\Mail\Message as ZendMessage; +use Zend\Mail\Transport\Sendmail; /** * Class that responsible for filling some message data before transporting it. - * @see Zend_Mail_Transport_Sendmail is used for transport + * @see \Zend\Mail\Transport\Sendmail is used for transport */ class Transport implements TransportInterface { @@ -29,79 +32,81 @@ class Transport implements TransportInterface const XML_PATH_SENDING_RETURN_PATH_EMAIL = 'system/smtp/return_path_email'; /** - * Object for sending eMails + * Whether return path should be set or no. * - * @var \Zend_Mail_Transport_Sendmail + * Possible values are: + * 0 - no + * 1 - yes (set value as FROM address) + * 2 - use custom value + * + * @var int */ - private $transport; + private $isSetReturnPath; /** - * Email message object that should be instance of \Zend_Mail - * - * @var MessageInterface + * @var string|null + */ + private $returnPathValue; + + /** + * @var Sendmail + */ + private $zendTransport; + + /** + * @var Message */ private $message; /** - * Core store config - * - * @var ScopeConfigInterface + * @var ZendMessage */ - private $scopeConfig; + private $zendMessage; /** - * @param \Zend_Mail_Transport_Sendmail $transport - * @param MessageInterface $message Email message object + * @param Message $message Email message object * @param ScopeConfigInterface $scopeConfig Core store config - * @param string|array|\Zend_Config|null $parameters Config options for sendmail parameters - * - * @throws \InvalidArgumentException when $message is not an instance of \Zend_Mail + * @param ZendMessage $zendMessage + * @param null|string|array|\Traversable $parameters Config options for sendmail parameters */ public function __construct( - \Zend_Mail_Transport_Sendmail $transport, - MessageInterface $message, - ScopeConfigInterface $scopeConfig + Message $message, + ScopeConfigInterface $scopeConfig, + ZendMessage $zendMessage, + $parameters = null ) { - if (!$message instanceof \Zend_Mail) { - throw new \InvalidArgumentException('The message should be an instance of \Zend_Mail'); - } - $this->transport = $transport; + $this->isSetReturnPath = (int) $scopeConfig->getValue( + self::XML_PATH_SENDING_SET_RETURN_PATH, + ScopeInterface::SCOPE_STORE + ); + $this->returnPathValue = $scopeConfig->getValue( + self::XML_PATH_SENDING_RETURN_PATH_EMAIL, + ScopeInterface::SCOPE_STORE + ); + + $this->zendTransport = new Sendmail($parameters); $this->message = $message; - $this->scopeConfig = $scopeConfig; + $this->zendMessage = $zendMessage; } /** - * Sets Return-Path to email if necessary, and sends email if it is allowed by System Configurations - * - * @return void - * @throws MailException + * @inheritdoc */ public function sendMessage() { try { - /* configuration of whether return path should be set or no. Possible values are: - * 0 - no - * 1 - yes (set value as FROM address) - * 2 - use custom value - * @see Magento\Config\Model\Config\Source\Yesnocustom - */ - $isSetReturnPath = $this->scopeConfig->getValue( - self::XML_PATH_SENDING_SET_RETURN_PATH, - ScopeInterface::SCOPE_STORE - ); - $returnPathValue = $this->scopeConfig->getValue( - self::XML_PATH_SENDING_RETURN_PATH_EMAIL, - ScopeInterface::SCOPE_STORE - ); - - if ($isSetReturnPath == '1') { - $this->message->setReturnPath($this->message->getFrom()); - } elseif ($isSetReturnPath == '2' && $returnPathValue !== null) { - $this->message->setReturnPath($returnPathValue); + $message = $this->zendMessage->fromString($this->message->getRawMessage()); + if (2 === $this->isSetReturnPath && $this->returnPathValue) { + $message->setSender($this->returnPathValue); + } elseif (1 === $this->isSetReturnPath && $message->getFrom()->count()) { + $fromAddressList = $message->getFrom(); + $fromAddressList->rewind(); + $message->setSender($fromAddressList->current()->getEmail()); } - $this->transport->send($this->message); + + $this->zendTransport->send($message); } catch (\Exception $e) { - throw new MailException(__($e->getMessage()), $e); + throw new MailException(new Phrase($e->getMessage()), $e); } } diff --git a/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php b/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php new file mode 100644 index 0000000000000..b06dc3cb48cbc --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Model\Plugin; + +use Magento\Email\Model\Plugin\WindowsSmtpConfig; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Mail\TransportInterface; +use Magento\Framework\OsInfo; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * WindowsSmtpConfigTest + */ +class WindowsSmtpConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var WindowsSmtpConfig + */ + private $windowsSmtpConfig; + + /** + * @var OsInfo|\PHPUnit_Framework_MockObject_MockObject + */ + private $osInfoMock; + + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var TransportInterface + */ + private $transportMock; + + /** + * setUp + * + * @return void + */ + public function setUp() + { + $objectManager = new ObjectManager($this); + + $this->osInfoMock = $this->createMock(OsInfo::class); + $this->configMock = $this->createMock(ReinitableConfigInterface::class); + $this->transportMock = $this->createMock(TransportInterface::class); + + $this->windowsSmtpConfig = $objectManager->getObject( + WindowsSmtpConfig::class, + [ + 'config' => $this->configMock, + 'osInfo' => $this->osInfoMock + ] + ); + } + + /** + * Test if SMTP settings if windows server + * + * @return void + */ + public function testBeforeSendMessageOsWindows() + { + $this->osInfoMock->expects($this->once()) + ->method('isWindows') + ->willReturn(true); + + $this->configMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturnMap([ + [WindowsSmtpConfig::XML_SMTP_HOST, '127.0.0.1'], + [WindowsSmtpConfig::XML_SMTP_PORT, '80'] + ]); + + $this->windowsSmtpConfig->beforeSendMessage($this->transportMock); + } + + /** + * Test if SMTP settings if not windows server + * + * @return void + */ + public function testBeforeSendMessageOsIsWindows() + { + $this->osInfoMock->expects($this->once()) + ->method('isWindows') + ->willReturn(false); + + $this->configMock->expects($this->never()) + ->method('getValue'); + + $this->windowsSmtpConfig->beforeSendMessage($this->transportMock); + } +} diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php new file mode 100644 index 0000000000000..8d2931f3e38f5 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Model\Template; + +use Magento\Email\Model\Template\SenderResolver; +use Magento\Framework\Exception\MailException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * SenderResolverTest + */ +class SenderResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SenderResolver + */ + private $senderResolver; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @return void + */ + public function setUp() + { + $objectManager = new ObjectManager($this); + + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + + $this->senderResolver = $objectManager->getObject( + SenderResolver::class, + [ + 'scopeConfig' => $this->scopeConfig + ] + ); + } + + /** + * Test returned information for given sender's name and email + * + * @return void + */ + public function testResolve() + { + $sender = 'general'; + $scopeId = null; + + $this->scopeConfig->expects($this->exactly(2)) + ->method('getValue') + ->willReturnMap([ + [ + 'trans_email/ident_' . $sender . '/name', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $scopeId, + 'Test Name' + ], + [ + 'trans_email/ident_' . $sender . '/email', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $scopeId, + 'test@email.com' + ] + ]); + + $result = $this->senderResolver->resolve($sender); + + $this->assertTrue(isset($result['name'])); + $this->assertEquals('Test Name', $result['name']); + + $this->assertTrue(isset($result['email'])); + $this->assertEquals('test@email.com', $result['email']); + } + + /** + * Test if exception is thrown in case there is no name or email in result + * + * @dataProvider dataProvidedSenderArray + * @param array $sender + * + * @return void + */ + public function testResolveThrowException(array $sender) + { + $this->expectExceptionMessage('Invalid sender data'); + $this->expectException(MailException::class); + $this->senderResolver->resolve($sender); + } + + /** + * @return array + */ + public function dataProvidedSenderArray() + { + return [ + [ + ['name' => 'Name'] + ], + [ + ['email' => 'test@email.com'] + ] + ]; + } +} diff --git a/app/code/Magento/Email/Test/Unit/Model/TransportTest.php b/app/code/Magento/Email/Test/Unit/Model/TransportTest.php deleted file mode 100644 index 3589e43996936..0000000000000 --- a/app/code/Magento/Email/Test/Unit/Model/TransportTest.php +++ /dev/null @@ -1,192 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Email\Test\Unit\Model; - -use Magento\Email\Model\Transport; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Mail\Message; -use Magento\Store\Model\ScopeInterface; - -/** - * Covers \Magento\Email\Model\Transport - */ -class TransportTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Zend_Mail_Transport_Sendmail|\PHPUnit_Framework_MockObject_MockObject - */ - private $transportMock; - - /** - * @var Message|\PHPUnit_Framework_MockObject_MockObject - */ - private $messageMock; - - /** - * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $scopeConfigMock; - - /** - * @var Transport - */ - private $model; - - protected function setUp() - { - $this->transportMock = $this->createMock(\Zend_Mail_Transport_Sendmail::class); - - $this->messageMock = $this->createMock(Message::class); - - $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - - $this->model = new Transport($this->transportMock, $this->messageMock, $this->scopeConfigMock); - } - - /** - * Tests that if any exception was caught, \Magento\Framework\Exception\MailException will thrown - * - * @expectedException \Magento\Framework\Exception\MailException - */ - public function testSendMessageException() - { - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->willThrowException(new \Exception('some exception')); - $this->model->sendMessage(); - } - - /** - * Tests that if sending Return-Path was disabled or email was not provided, - this header won't be set - * - * @param string|int|null $returnPathSet - * @param string|null $returnPathEmail - * - * @dataProvider sendMessageWithoutReturnPathDataProvider - */ - public function testSendMessageWithoutReturnPath($returnPathSet, $returnPathEmail = null) - { - $this->prepareSendingMessage($returnPathSet, $returnPathEmail); - - $this->messageMock->expects($this->never()) - ->method('setReturnPath'); - $this->transportMock->expects($this->once()) - ->method('send'); - $this->model->sendMessage(); - } - - /** - * Tests that if sending Return-Path was disabled, this header won't be set - * - * @param string|int|null $returnPathSet - * @param string|null $emailFrom - * - * @dataProvider sendMessageWithDefaultReturnPathDataProvider - */ - public function testSendMessageWithDefaultReturnPath($returnPathSet, $emailFrom) - { - $this->prepareSendingMessage($returnPathSet, null); - - $this->messageMock->expects($this->once()) - ->method('setReturnPath') - ->with($emailFrom); - $this->messageMock->expects($this->once()) - ->method('getFrom') - ->willReturn($emailFrom); - $this->transportMock->expects($this->once()) - ->method('send'); - $this->model->sendMessage(); - } - - /** - * Tests that if sending Return-Path was disabled, this header won't be set - * - * @param string|int|null $returnPathSet - * @param string|null $emailFrom - * - * @dataProvider sendMessageWithCustomReturnPathDataProvider - */ - public function testSendMessageWithCustomReturnPath($returnPathSet, $emailFrom) - { - $this->prepareSendingMessage($returnPathSet, $emailFrom); - - $this->messageMock->expects($this->once()) - ->method('setReturnPath') - ->with($emailFrom); - $this->messageMock->expects($this->never()) - ->method('getFrom') - ->willReturn($emailFrom); - $this->transportMock->expects($this->once()) - ->method('send'); - $this->model->sendMessage(); - } - - /** - * Tests retrieving message object - */ - public function testGetMessage() - { - $this->assertEquals($this->messageMock, $this->model->getMessage()); - } - - /** - * Executes all main sets for sending message - * - * @param string|int|null $returnPathSet - * @param string|null $returnPathEmail - */ - private function prepareSendingMessage($returnPathSet, $returnPathEmail) - { - $map = [ - [Transport::XML_PATH_SENDING_SET_RETURN_PATH, ScopeInterface::SCOPE_STORE, null, $returnPathSet], - [Transport::XML_PATH_SENDING_RETURN_PATH_EMAIL, ScopeInterface::SCOPE_STORE, null, $returnPathEmail] - ]; - $this->scopeConfigMock->expects($this->exactly(2)) - ->method('getValue') - ->willReturnMap($map); - } - - /** - * Data provider for testSendMessageWithoutReturnPath - * @return array - */ - public function sendMessageWithoutReturnPathDataProvider() - { - return [ - [0], - ['0'], - [3], - ['2', null], - [2, null], - ]; - } - - /** - * Data provider for testSendMessageWithDefaultReturnPath - * @return array - */ - public function sendMessageWithDefaultReturnPathDataProvider() - { - return [ - [1, 'test@exemple.com'], - ['1', 'test@exemple.com'], - ['1', ''] - ]; - } - - /** - * Data provider for testSendMessageWithCustomReturnPath - * @return array - */ - public function sendMessageWithCustomReturnPathDataProvider() - { - return [ - [2, 'test@exemple.com'], - ['2', 'test@exemple.com'], - ['2', ''] - ]; - } -} diff --git a/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php new file mode 100644 index 0000000000000..e7d9277593350 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GiftMessage\Test\Unit\Observer; + +use Magento\GiftMessage\Observer\SalesEventQuoteMerge; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; + +/** + * SalesEventQuoteMergeTest + */ +class SalesEventQuoteMergeTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var SalesEventQuoteMerge + */ + private $salesEventQuoteMerge; + + /** + * @return void + */ + public function setUp() + { + $objectManger = new ObjectManager($this); + $this->salesEventQuoteMerge = $objectManger->getObject(SalesEventQuoteMerge::class); + } + + /** + * @dataProvider dataProviderGiftMessageId + * + * @param null|int $giftMessageId + * + * @return void + */ + public function testExecute($giftMessageId) + { + $sourceQuoteMock = $this->createPartialMock(Quote::class, ['getGiftMessageId']); + $sourceQuoteMock->expects($this->once()) + ->method('getGiftMessageId') + ->willReturn($giftMessageId); + + $targetQuoteMock = $this->createPartialMock(Quote::class, ['setGiftMessageId']); + + if ($giftMessageId) { + $targetQuoteMock->expects($this->once()) + ->method('setGiftMessageId'); + } else { + $targetQuoteMock->expects($this->never()) + ->method('setGiftMessageId'); + } + + $observer = $this->createMock(Observer::class); + $observer->expects($this->exactly(2)) + ->method('getData') + ->willReturnMap([ + ['quote', null, $targetQuoteMock], + ['source', null, $sourceQuoteMock] + ]); + + $this->salesEventQuoteMerge->execute($observer); + } + + /** + * @return array + */ + public function dataProviderGiftMessageId(): array + { + return [ + [null], + [1] + ]; + } +} diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml new file mode 100644 index 0000000000000..e760b877fa33d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ApiGroupedProduct" type="product3"> + <data key="sku" unique="suffix">api-grouped-product</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">Api Grouped Product</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-grouped-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkData.xml new file mode 100644 index 0000000000000..362e595593783 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkData.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductLinkSimple1" type="product_link"> + <var key="sku" entityKey="sku" entityType="product3"/> + <var key="linked_product_sku" entityKey="sku" entityType="product"/> + <data key="link_type">associated</data> + <data key="linked_product_type">simple</data> + <data key="position">1</data> + <requiredEntity type="product_link_extension_attribute">Qty1000</requiredEntity> + </entity> + <entity name="ProductLinkSimple2" type="product_link" extends="ProductLinkSimple1"> + <data key="position">2</data> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkExtensionAttributeData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkExtensionAttributeData.xml new file mode 100644 index 0000000000000..b580c876a6f30 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinkExtensionAttributeData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Qty1000" type="product_link_extension_attribute"> + <data key="qty">1000</data> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinksData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinksData.xml new file mode 100644 index 0000000000000..68c95e856e2f8 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/ProductLinksData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="OneSimpleProductLink" type="product_links"> + <requiredEntity type="product_link">ProductLinkSimple1</requiredEntity> + </entity> + <entity name="OneMoreSimpleProductLink" type="product_links"> + <requiredEntity type="product_link">ProductLinkSimple2</requiredEntity> + </entity> + <entity name="TwoSimpleProductLinks" type="product_links"> + <array key="items"> + <item>ProductLinkSimple1</item> + <item>ProductLinkSimple2</item> + </array> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Metadata/product-meta.xml new file mode 100644 index 0000000000000..87fa0a6379dd3 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Metadata/product-meta.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <!-- Data type product3 is to work around MQE-1035 --> + <operation name="CreateProduct3" dataType="product3" type="create" auth="adminOauth" url="/V1/products" method="POST"> + <contentType>application/json</contentType> + <object dataType="product3" key="product"> + <field key="sku">string</field> + <field key="name">string</field> + <field key="attribute_set_id">integer</field> + <field key="price">number</field> + <field key="status">integer</field> + <field key="visibility">integer</field> + <field key="type_id">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="weight">integer</field> + <field key="extension_attributes">product_extension_attribute</field> + <array key="product_links"> + <value>product_link</value> + </array> + <array key="custom_attributes"> + <value>custom_attribute_array</value> + </array> + <array key="options"> + <value>product_option</value> + </array> + </object> + </operation> + <!-- Data type product3 is to work around MQE-1035 --> + <operation name="DeleteProduct3" dataType="product3" type="delete" auth="adminOauth" url="/V1/products/{sku}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetProduct3" dataType="product3" type="get" auth="adminOauth" url="/V1/products/{sku}" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Integration/Model/CustomerTokenService.php b/app/code/Magento/Integration/Model/CustomerTokenService.php index 947ca6f9f8821..adacf5ebacf71 100644 --- a/app/code/Magento/Integration/Model/CustomerTokenService.php +++ b/app/code/Magento/Integration/Model/CustomerTokenService.php @@ -14,6 +14,7 @@ use Magento\Integration\Model\ResourceModel\Oauth\Token\CollectionFactory as TokenCollectionFactory; use Magento\Integration\Model\Oauth\Token\RequestThrottler; use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Event\ManagerInterface; class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServiceInterface { @@ -48,6 +49,11 @@ class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServ */ private $requestThrottler; + /** + * @var Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + /** * Initialize service * @@ -55,17 +61,21 @@ class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServ * @param AccountManagementInterface $accountManagement * @param TokenCollectionFactory $tokenModelCollectionFactory * @param \Magento\Integration\Model\CredentialsValidator $validatorHelper + * @param \Magento\Framework\Event\ManagerInterface $eventManager */ public function __construct( TokenModelFactory $tokenModelFactory, AccountManagementInterface $accountManagement, TokenCollectionFactory $tokenModelCollectionFactory, - CredentialsValidator $validatorHelper + CredentialsValidator $validatorHelper, + ManagerInterface $eventManager = null ) { $this->tokenModelFactory = $tokenModelFactory; $this->accountManagement = $accountManagement; $this->tokenModelCollectionFactory = $tokenModelCollectionFactory; $this->validatorHelper = $validatorHelper; + $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ManagerInterface::class); } /** @@ -83,6 +93,7 @@ public function createCustomerAccessToken($username, $password) __('You did not sign in correctly or your account is temporarily disabled.') ); } + $this->eventManager->dispatch('customer_login', ['customer' => $customerDataObject]); $this->getRequestThrottler()->resetAuthenticationFailuresCount($username, RequestThrottler::USER_TYPE_CUSTOMER); return $this->tokenModelFactory->create()->createCustomerToken($customerDataObject->getId())->getToken(); } diff --git a/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php b/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php index ecd4788545c0a..170e7e42d919e 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php @@ -32,6 +32,9 @@ class CustomerTokenServiceTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Integration\Model\Oauth\Token|\PHPUnit_Framework_MockObject_MockObject */ private $_tokenMock; + /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $manager; + protected function setUp() { $this->_tokenFactoryMock = $this->getMockBuilder(\Magento\Integration\Model\Oauth\TokenFactory::class) @@ -67,11 +70,14 @@ protected function setUp() \Magento\Integration\Model\CredentialsValidator::class )->disableOriginalConstructor()->getMock(); + $this->manager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->_tokenService = new \Magento\Integration\Model\CustomerTokenService( $this->_tokenFactoryMock, $this->_accountManagementMock, $this->_tokenModelCollectionFactoryMock, - $this->validatorHelperMock + $this->validatorHelperMock, + $this->manager ); } diff --git a/app/code/Magento/Integration/etc/webapi_rest/events.xml b/app/code/Magento/Integration/etc/webapi_rest/events.xml new file mode 100644 index 0000000000000..e978698734277 --- /dev/null +++ b/app/code/Magento/Integration/etc/webapi_rest/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="customer_login"> + <observer name="customer_log_login" instance="Magento\Customer\Observer\LogLastLoginAtObserver" /> + </event> +</config> diff --git a/app/code/Magento/Integration/etc/webapi_soap/events.xml b/app/code/Magento/Integration/etc/webapi_soap/events.xml new file mode 100644 index 0000000000000..e978698734277 --- /dev/null +++ b/app/code/Magento/Integration/etc/webapi_soap/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="customer_login"> + <observer name="customer_log_login" instance="Magento\Customer\Observer\LogLastLoginAtObserver" /> + </event> +</config> diff --git a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php index 9cdc90bc46b2a..78c485c5bb6f5 100644 --- a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php +++ b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php @@ -64,6 +64,7 @@ public function report() $moduleData = $this->collect->getModuleData(); if (count($moduleData['changes']) > 0) { foreach ($moduleData['changes'] as $change) { + $modelData = []; switch ($change['type']) { case Config::ENABLED: $modelData = [ diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php index ec21e06976b8b..9882a1ce9b0b8 100644 --- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php +++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php @@ -31,7 +31,7 @@ public function addCustomParameter($param, $value) /** * Wrapper for 'newrelic_notice_error' function * - * @param Exception $exception + * @param \Exception $exception * @return void */ public function reportError($exception) diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php index efc469e15deaa..ed415d04450a6 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php @@ -4,13 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; +/** + * Controller for unsubscribing customers. + */ class Unsubscribe extends \Magento\Newsletter\Controller\Subscriber { /** * Unsubscribe newsletter - * @return void + * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() { @@ -27,6 +31,10 @@ public function execute() $this->messageManager->addException($e, __('Something went wrong while unsubscribing you.')); } } - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + /** @var \Magento\Backend\Model\View\Result\Redirect $redirect */ + $redirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + $redirectUrl = $this->_redirect->getRedirectUrl(); + + return $redirect->setUrl($redirectUrl); } } diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index b916f20dbe770..5e0d5448f11ca 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -594,6 +594,8 @@ protected function _updateCustomerSubscription($customerId, $subscribe) } elseif (($this->getStatus() == self::STATUS_UNCONFIRMED) && ($customerData->getConfirmation() === null)) { $status = self::STATUS_SUBSCRIBED; $sendInformationEmail = true; + } elseif (($this->getStatus() == self::STATUS_NOT_ACTIVE) && ($customerData->getConfirmation() === null)) { + $status = self::STATUS_NOT_ACTIVE; } else { $status = self::STATUS_UNSUBSCRIBED; } diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index b67eb1b45fe45..cead9f838f4c0 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -60,6 +60,7 @@ class Tablerate extends \Magento\Shipping\Model\Carrier\AbstractCarrier implemen * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $resultMethodFactory * @param \Magento\OfflineShipping\Model\ResourceModel\Carrier\TablerateFactory $tablerateFactory * @param array $data + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function __construct( diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 4639a2705a289..f0b86588f1cfa 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -14,8 +14,8 @@ use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** - * Wrapper that performs Paypal Express and Checkout communication - * Use current Paypal Express method instance + * Wrapper that performs Paypal Express and Checkout communication. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -24,6 +24,7 @@ class Checkout { /** * Cache ID prefix for "pal" lookup + * * @var string */ const PAL_CACHE_ID = 'paypal_express_checkout_pal'; @@ -364,8 +365,9 @@ public function __construct( } /** - * Checkout with PayPal image URL getter - * Spares API calls of getting "pal" variable, by putting it into cache per store view + * Checkout with PayPal image URL getter. + * + * Spares API calls of getting "pal" variable, by putting it into cache per store view. * * @return string */ @@ -597,8 +599,8 @@ public function canSkipOrderReviewStep() /** * Update quote when returned from PayPal - * rewrite billing address by paypal - * save old billing address for new customer + * + * Rewrite billing address by paypal, save old billing address for new customer, and * export shipping address in case address absence * * @param string $token @@ -614,14 +616,15 @@ public function returnFromPaypal($token) $this->ignoreAddressValidation(); + // check if we came from the Express Checkout button + $isButton = (bool)$quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON); + // import shipping address $exportedShippingAddress = $this->_getApi()->getExportedShippingAddress(); if (!$quote->getIsVirtual()) { $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress) { - if ($exportedShippingAddress - && $quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1 - ) { + if ($exportedShippingAddress && $isButton) { $this->_setExportedAddressData($shippingAddress, $exportedShippingAddress); // PayPal doesn't provide detailed shipping info: prefix, middlename, lastname, suffix $shippingAddress->setPrefix(null); @@ -649,12 +652,11 @@ public function returnFromPaypal($token) } // import billing address - $portBillingFromShipping = $quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1 - && $this->_config->getValue( - 'requireBillingAddress' - ) != \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL - && !$quote->isVirtual(); - if ($portBillingFromShipping) { + $requireBillingAddress = (int)$this->_config->getValue( + 'requireBillingAddress' + ) === \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL; + + if ($isButton && !$requireBillingAddress && !$quote->isVirtual()) { $billingAddress = clone $shippingAddress; $billingAddress->unsAddressId()->unsAddressType()->setCustomerAddressId(null); $data = $billingAddress->getData(); @@ -662,11 +664,17 @@ public function returnFromPaypal($token) $quote->getBillingAddress()->addData($data); $quote->getShippingAddress()->setSameAsBilling(1); } else { - $billingAddress = $quote->getBillingAddress(); + $billingAddress = $quote->getBillingAddress()->setCustomerAddressId(null); } $exportedBillingAddress = $this->_getApi()->getExportedBillingAddress(); - $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); + // Since country is required field for billing and shipping address, + // we consider the address information to be empty if country is empty. + $isEmptyAddress = ($billingAddress->getCountryId() === null); + + if ($requireBillingAddress || $isEmptyAddress) { + $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); + } $billingAddress->setCustomerNote($exportedBillingAddress->getData('note')); $quote->setBillingAddress($billingAddress); $quote->setCheckoutMethod($this->getCheckoutMethod()); @@ -894,7 +902,7 @@ public function getCheckoutMethod() } /** - * Sets address data from exported address + * Sets address data from exported address. * * @param Address $address * @param array $exportedAddress @@ -902,17 +910,6 @@ public function getCheckoutMethod() */ protected function _setExportedAddressData($address, $exportedAddress) { - // Exported data is more priority if we came from Express Checkout button - $isButton = (bool)$this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON); - - // Since country is required field for billing and shipping address, - // we consider the address information to be empty if country is empty. - $isEmptyAddress = ($address->getCountryId() === null); - - if (!$isButton && !$isEmptyAddress) { - return; - } - foreach ($exportedAddress->getExportedKeys() as $key) { $data = $exportedAddress->getData($key); if (!empty($data)) { @@ -949,9 +946,11 @@ protected function _setBillingAgreementRequest() } /** + * Get api + * * @return \Magento\Paypal\Model\Api\Nvp */ - protected function _getApi() + protected function _getApi(): \Magento\Paypal\Model\Api\Nvp { if (null === $this->_api) { $this->_api = $this->_apiTypeFactory->create($this->_apiType)->setConfigObject($this->_config); @@ -960,9 +959,10 @@ protected function _getApi() } /** - * Attempt to collect address shipping rates and return them for further usage in instant update API - * Returns empty array if it was impossible to obtain any shipping rate - * If there are shipping rates obtained, the method must return one of them as default. + * Attempt to collect address shipping rates and return them for further usage in instant update API. + * + * Returns empty array if it was impossible to obtain any shipping rate and + * if there are shipping rates obtained, the method must return one of them as default. * * @param Address $address * @param bool $mayReturnEmpty @@ -1043,22 +1043,23 @@ protected function _prepareShippingOptions(Address $address, $mayReturnEmpty = f } /** - * Compare two shipping options based on their amounts + * Compare two shipping options based on their amounts. * - * This function is used as a callback comparison function in shipping options sorting process - * @see self::_prepareShippingOptions() + * This function is used as a callback comparison function in shipping options sorting process. * + * @see self::_prepareShippingOptions() * @param \Magento\Framework\DataObject $option1 * @param \Magento\Framework\DataObject $option2 * @return int */ - protected static function cmpShippingOptions(DataObject $option1, DataObject $option2) + protected static function cmpShippingOptions(DataObject $option1, DataObject $option2): int { return $option1->getAmount() <=> $option2->getAmount(); } /** - * Try to find whether the code provided by PayPal corresponds to any of possible shipping rates + * Try to find whether the code provided by PayPal corresponds to any of possible shipping rates. + * * This method was created only because PayPal has issues with returning the selected code. * If in future the issue is fixed, we don't need to attempt to match it. It would be enough to set the method code * before collecting shipping rates @@ -1067,7 +1068,7 @@ protected static function cmpShippingOptions(DataObject $option1, DataObject $op * @param string $selectedCode * @return string */ - protected function _matchShippingMethodCode(Address $address, $selectedCode) + protected function _matchShippingMethodCode(Address $address, $selectedCode): string { $options = $this->_prepareShippingOptions($address, false); foreach ($options as $option) { @@ -1083,7 +1084,8 @@ protected function _matchShippingMethodCode(Address $address, $selectedCode) } /** - * Create payment redirect url + * Create payment redirect url. + * * @param bool|null $button * @param string $token * @return void @@ -1106,7 +1108,8 @@ public function getCustomerSession() } /** - * Set shipping options to api + * Set shipping options to api. + * * @param \Magento\Paypal\Model\Cart $cart * @param \Magento\Quote\Model\Quote\Address|null $address * @return void diff --git a/app/code/Magento/ProductVideo/i18n/de_DE.csv b/app/code/Magento/ProductVideo/i18n/de_DE.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/de_DE.csv +++ b/app/code/Magento/ProductVideo/i18n/de_DE.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/en_US.csv b/app/code/Magento/ProductVideo/i18n/en_US.csv index 2d226c6daefa3..debcab151cc91 100644 --- a/app/code/Magento/ProductVideo/i18n/en_US.csv +++ b/app/code/Magento/ProductVideo/i18n/en_US.csv @@ -40,3 +40,4 @@ Delete,Delete "Autostart base video","Autostart base video" "Show related video","Show related video" "Auto restart video","Auto restart video" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/es_ES.csv b/app/code/Magento/ProductVideo/i18n/es_ES.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/es_ES.csv +++ b/app/code/Magento/ProductVideo/i18n/es_ES.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/fr_FR.csv b/app/code/Magento/ProductVideo/i18n/fr_FR.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/fr_FR.csv +++ b/app/code/Magento/ProductVideo/i18n/fr_FR.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/nl_NL.csv b/app/code/Magento/ProductVideo/i18n/nl_NL.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/nl_NL.csv +++ b/app/code/Magento/ProductVideo/i18n/nl_NL.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/pt_BR.csv b/app/code/Magento/ProductVideo/i18n/pt_BR.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/pt_BR.csv +++ b/app/code/Magento/ProductVideo/i18n/pt_BR.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv index 7047317396999..ca24668bb8d16 100644 --- a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv +++ b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv @@ -7,3 +7,4 @@ "Preview Image","Preview Image" "Get Video Information","Get Video Information" "Youtube or Vimeo supported","Youtube or Vimeo supported" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml index f5a22c50e6d0d..63bd5321ad30b 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml +++ b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <css src="Magento_ProductVideo::css/gallery-delete-tooltip.css"/> + </head> <body> <referenceContainer name="content"> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index f0ae057bc724d..2ceb9e9c9d4f2 100755 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -34,7 +34,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to class="gallery" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" - data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" + data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" data-types="<?= $block->escapeHtml( $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) ) ?>" @@ -140,30 +140,37 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to alt="<%- data.label %>"/> <div class="actions"> - <button type="button" - class="action-remove" - data-role="delete-button" - title="<% if (data.media_type == 'external-video') {%> + <div class="tooltip"> + <span class="delete-tooltiptext"> + <?= $block->escapeHtml( + __('Delete image in all store views') + ); ?> + </span> + <button type="button" + class="action-remove" + data-role="delete-button" + title="<% if (data.media_type == 'external-video') {%> + <?= $block->escapeHtml( + __('Delete video') + ); ?> + <%} else {%> + <?= $block->escapeHtml( + __('Delete image') + ); ?> + <%}%>"> + <span> + <% if (data.media_type == 'external-video') { %> <?= $block->escapeHtml( __('Delete video') ); ?> - <%} else {%> + <% } else {%> <?= $block->escapeHtml( __('Delete image') ); ?> - <%}%>"> - <span> - <% if (data.media_type == 'external-video') { %> - <?= $block->escapeHtml( - __('Delete video') - ); ?> - <% } else {%> - <?= $block->escapeHtml( - __('Delete image') - ); ?> - <%} %> - </span> - </button> + <%} %> + </span> + </button> + </div> <div class="draggable-handle"></div> </div> <div class="image-fade"><span><?= $block->escapeHtml( diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css new file mode 100644 index 0000000000000..835a22683b157 --- /dev/null +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +.gallery .tooltip .delete-tooltiptext { + visibility: hidden; + width: 112px; + background-color: #373330; + color: #F7F3EB; + text-align: center; + padding: 5px 0; + position: absolute; + z-index: 1; + left: 30px; + top: 91px; +} + +.gallery .tooltip:hover .delete-tooltiptext { + visibility: visible; +} diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index 2cbca917c26a1..c89fc37b44689 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -76,6 +76,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); + $address->setCustomerId($quote->getCustomerId()); $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($address); try { diff --git a/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php new file mode 100644 index 0000000000000..f18bb46fa63fb --- /dev/null +++ b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +/** + * Remove quote items after mass disabling products + */ +class MarkQuotesRecollectMassDisabled +{ + /** @var QuoteResource$quoteResource */ + private $quoteResource; + + /** + * @param QuoteResource $quoteResource + */ + public function __construct( + QuoteResource $quoteResource + ) { + $this->quoteResource = $quoteResource; + } + + /** + * Clean quote items after mass disabling product + * + * @param \Magento\Catalog\Model\Product\Action $subject + * @param \Magento\Catalog\Model\Product\Action $result + * @param int[] $productIds + * @param int[] $attrData + * @param int $storeId + * @return \Magento\Catalog\Model\Product\Action + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateAttributes( + ProductAction $subject, + ProductAction $result, + $productIds, + $attrData, + $storeId + ): ProductAction { + if (isset($attrData['status']) && $attrData['status'] === Status::STATUS_DISABLED) { + $this->quoteResource->markQuotesRecollect($productIds); + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php index 8fa03232a0e8d..8f18a04a102fa 100644 --- a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php +++ b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php @@ -203,11 +203,12 @@ protected function _validateCouponCode(\Magento\Quote\Model\Quote $quote) */ protected function _collectItemsQtys(\Magento\Quote\Model\Quote $quote) { + $quoteItems = $quote->getAllVisibleItems(); $quote->setItemsCount(0); $quote->setItemsQty(0); $quote->setVirtualItemsQty(0); - foreach ($quote->getAllVisibleItems() as $item) { + foreach ($quoteItems as $item) { if ($item->getParentItem()) { continue; } 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 ef1705e0ad100..309c89e3702f5 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -3,9 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model\ResourceModel\Quote\Item; -use \Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\ResourceModel\Quote\Item as ResourceQuoteItem; /** * Quote item resource collection @@ -50,6 +57,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionContro */ private $storeManager; + /** + * @var bool $recollectQuote + */ + private $recollectQuote = false; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -102,7 +114,7 @@ public function __construct( */ protected function _construct() { - $this->_init(\Magento\Quote\Model\Quote\Item::class, \Magento\Quote\Model\ResourceModel\Quote\Item::class); + $this->_init(QuoteItem::class, ResourceQuoteItem::class); } /** @@ -110,7 +122,7 @@ protected function _construct() * * @return int */ - public function getStoreId() + public function getStoreId(): int { // Fallback to current storeId if no quote is provided // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721) @@ -119,12 +131,12 @@ public function getStoreId() } /** - * Set Quote object to Collection + * Set Quote object to Collection. * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @return $this */ - public function setQuote($quote) + public function setQuote($quote): self { $this->_quote = $quote; $quoteId = $quote->getId(); @@ -138,14 +150,15 @@ public function setQuote($quote) } /** - * Reset the collection and inner join it to quotes table + * Reset the collection and inner join it to quotes table. + * * Optionally can select items with specified product id only * * @param string $quotesTableName * @param int $productId * @return $this */ - public function resetJoinQuotes($quotesTableName, $productId = null) + public function resetJoinQuotes($quotesTableName, $productId = null): self { $this->getSelect()->reset()->from( ['qi' => $this->getResource()->getMainTable()], @@ -162,11 +175,11 @@ public function resetJoinQuotes($quotesTableName, $productId = null) } /** - * After load processing + * After load processing. * * @return $this */ - protected function _afterLoad() + protected function _afterLoad(): self { parent::_afterLoad(); @@ -195,11 +208,11 @@ protected function _afterLoad() } /** - * Add options to items + * Add options to items. * * @return $this */ - protected function _assignOptions() + protected function _assignOptions(): self { $itemIds = array_keys($this->_items); $optionCollection = $this->_itemOptionCollectionFactory->create()->addItemFilter($itemIds); @@ -213,12 +226,12 @@ protected function _assignOptions() } /** - * Add products to items and item options + * Add products to items and item options. * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _assignProducts() + protected function _assignProducts(): self { \Magento\Framework\Profiler::start('QUOTE:' . __METHOD__, ['group' => 'QUOTE', 'method' => __METHOD__]); $productCollection = $this->_productCollectionFactory->create()->setStoreId( @@ -240,46 +253,30 @@ protected function _assignProducts() ['collection' => $productCollection] ); - $recollectQuote = false; foreach ($this as $item) { + /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); - if ($product) { + $isValidProduct = $this->isValidProduct($product); + $qtyOptions = []; + if ($isValidProduct) { $product->setCustomOptions([]); - $qtyOptions = []; - $optionProductIds = []; - foreach ($item->getOptions() as $option) { - /** - * Call type-specific logic for product associated with quote item - */ - $product->getTypeInstance()->assignProductToOption( - $productCollection->getItemById($option->getProductId()), - $option, - $product - ); - - if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { - $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); - } - } - - if ($optionProductIds) { - foreach ($optionProductIds as $optionProductId) { - $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); - if ($qtyOption) { - $qtyOptions[$optionProductId] = $qtyOption; - } + $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); + foreach ($optionProductIds as $optionProductId) { + $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); + if ($qtyOption) { + $qtyOptions[$optionProductId] = $qtyOption; } } - - $item->setQtyOptions($qtyOptions)->setProduct($product); } else { $item->isDeleted(true); - $recollectQuote = true; + $this->recollectQuote = true; + } + if (!$item->isDeleted()) { + $item->setQtyOptions($qtyOptions)->setProduct($product); + $item->checkData(); } - $item->checkData(); } - - if ($recollectQuote && $this->_quote) { + if ($this->recollectQuote && $this->_quote) { $this->_quote->collectTotals(); } \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__); @@ -287,6 +284,57 @@ protected function _assignProducts() return $this; } + /** + * Get product Ids from option. + * + * @param QuoteItem $item + * @param ProductInterface $product + * @param ProductCollection $productCollection + * @return array + */ + private function getOptionProductIds( + QuoteItem $item, + ProductInterface $product, + ProductCollection $productCollection + ): array { + $optionProductIds = []; + foreach ($item->getOptions() as $option) { + /** + * Call type-specific logic for product associated with quote item + */ + $product->getTypeInstance()->assignProductToOption( + $productCollection->getItemById($option->getProductId()), + $option, + $product + ); + + if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { + $isValidProduct = $this->isValidProduct($option->getProduct()); + if (!$isValidProduct && !$item->isDeleted()) { + $item->isDeleted(true); + $this->recollectQuote = true; + continue; + } + $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); + } + } + + return $optionProductIds; + } + + /** + * Check is valid product. + * + * @param ProductInterface $product + * @return bool + */ + private function isValidProduct(ProductInterface $product): bool + { + $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); + + return $result; + } + /** * Prevents adding stock status filter to the collection of products. * diff --git a/app/code/Magento/Quote/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.xml b/app/code/Magento/Quote/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.xml new file mode 100644 index 0000000000000..dba4a94f3db2a --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Disabled a product by filtering grid and using change status action--> + <actionGroup name="ChangeStatusProductUsingProductGridActionGroup"> + <arguments> + <argument name="product"/> + <argument name="status" defaultValue="Enable" type="string" /> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickChangeStatusAction"/> + <click selector="{{AdminProductGridSection.changeStatus('status')}}" stepKey="clickChangeStatusDisabled" parameterized="true"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) have been updated." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Quote/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Quote/Test/Mftf/Section/AdminProductGridSection.xml new file mode 100644 index 0000000000000..32ac73aca7c03 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Section/AdminProductGridSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminProductGridSection"> + <element name="changeStatus" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-menu-item')]//ul/li/span[text() = '{{status}}']" stepKey="clickChangeStatus" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml new file mode 100644 index 0000000000000..0c20eb54d5446 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.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="StorefrontGuestCheckoutDisabledProductTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Remove item from cart if product disabled"/> + <description value="Remove item from cart if simple or configurable product is disabled"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95844"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + <!-- Step 1: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> + <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart($$createConfigProduct.name$$)}}" time="30" stepKey="assertMessage"/> + <!--Disabled via admin panel--> + <openNewTab stepKey="openNewTab"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Find the first simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!-- Disabled child configurable product --> + <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductPageSaved"/> + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab"/> + <!--Check cart--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem"/> + </test> +</tests> diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index ecc6426855679..8add3786eb9a3 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -96,6 +96,9 @@ <plugin name="clean_quote_items_after_product_delete" type="Magento\Quote\Model\Product\Plugin\RemoveQuoteItems"/> <plugin name="update_quote_items_after_product_save" type="Magento\Quote\Model\Product\Plugin\UpdateQuoteItems"/> </type> + <type name="Magento\Catalog\Model\Product\Action"> + <plugin name="quoteProductMassChange" type="Magento\Quote\Model\Product\Plugin\MarkQuotesRecollectMassDisabled"/> + </type> <type name="Magento\Quote\Model\ValidationRules\QuoteValidationComposite"> <arguments> <argument name="validationRules" xsi:type="array"> diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php index 671acc9701012..7b3e165687bac 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php @@ -5,7 +5,11 @@ */ namespace Magento\Reports\Model\ResourceModel\Quote; +use Magento\Store\Model\Store; + /** + * Collection of abandoned quotes with reports join. + * * @api * @since 100.0.2 */ @@ -48,6 +52,24 @@ public function __construct( $this->customerResource = $customerResource; } + /** + * Filter collections by stores. + * + * @param array $storeIds + * @param bool $withAdmin + * @return $this + */ + public function addStoreFilter(array $storeIds, bool $withAdmin = true) + { + if ($withAdmin) { + $storeIds[] = Store::DEFAULT_STORE_ID; + } + + $this->addFieldToFilter('store_id', ['in' => $storeIds]); + + return $this; + } + /** * Prepare for abandoned report * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php index 1912655a9292d..10b80b6f4e527 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php @@ -88,7 +88,8 @@ public function getStatuses() */ public function canSendCommentEmail() { - return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()); + return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()) + && $this->_authorization->isAllowed('Magento_Sales::email'); } /** diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index c30bb5e328e8b..12d2a5396ddc4 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -16,6 +16,7 @@ use Magento\Quote\Model\Quote\Item; use Magento\Sales\Api\Data\OrderAddressInterface; use Magento\Sales\Model\Order; +use Magento\Store\Model\StoreManagerInterface; /** * Order create model @@ -243,6 +244,11 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ */ private $dataObjectConverter; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -274,6 +280,7 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param ExtensibleDataObjectConverter|null $dataObjectConverter + * @param StoreManagerInterface $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -306,7 +313,8 @@ public function __construct( \Magento\Quote\Model\QuoteFactory $quoteFactory, array $data = [], \Magento\Framework\Serialize\Serializer\Json $serializer = null, - ExtensibleDataObjectConverter $dataObjectConverter = null + ExtensibleDataObjectConverter $dataObjectConverter = null, + StoreManagerInterface $storeManager = null ) { $this->_objectManager = $objectManager; $this->_eventManager = $eventManager; @@ -340,6 +348,7 @@ public function __construct( parent::__construct($data); $this->dataObjectConverter = $dataObjectConverter ?: ObjectManager::getInstance() ->get(ExtensibleDataObjectConverter::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -417,7 +426,8 @@ public function setRecollect($flag) /** * Recollect totals for customer cart. - * Set recollect totals flag for quote + * + * Set recollect totals flag for quote. * * @return $this */ @@ -1327,6 +1337,7 @@ protected function _createCustomerForm(\Magento\Customer\Api\Data\CustomerInterf /** * Set and validate Quote address + * * All errors added to _errors * * @param \Magento\Quote\Model\Quote\Address $address @@ -1530,6 +1541,8 @@ public function resetShippingMethod() */ public function collectShippingRates() { + $store = $this->getQuote()->getStore(); + $this->storeManager->setCurrentStore($store); $this->getQuote()->getShippingAddress()->setCollectShippingRates(true); $this->collectRates(); diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php index f00334f496b2a..f644d0c3a5a63 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php @@ -37,6 +37,8 @@ public function __construct( } /** + * Collects credit memo shipping totals. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @throws \Magento\Framework\Exception\LocalizedException @@ -55,12 +57,10 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $orderShippingInclTax = $order->getShippingInclTax(); $orderBaseShippingInclTax = $order->getBaseShippingInclTax(); $allowedTaxAmount = $order->getShippingTaxAmount() - $order->getShippingTaxRefunded(); - $baseAllowedTaxAmount = $order->getBaseShippingTaxAmount() - $order->getBaseShippingTaxRefunded(); $allowedAmountInclTax = $allowedAmount + $allowedTaxAmount; - $baseAllowedAmountInclTax = $baseAllowedAmount + $baseAllowedTaxAmount; - - // for the credit memo - $shippingAmount = $baseShippingAmount = $shippingInclTax = $baseShippingInclTax = 0; + $baseAllowedAmountInclTax = $orderBaseShippingInclTax + - $order->getBaseShippingRefunded() + - $order->getBaseShippingTaxRefunded(); // Check if the desired shipping amount to refund was specified (from invoice or another source). if ($creditmemo->hasBaseShippingAmount()) { @@ -128,7 +128,6 @@ private function isSuppliedShippingAmountInclTax($order) /** * Get the Tax Config. - * In a future release, will become a constructor parameter. * * @return \Magento\Tax\Model\Config * 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 a842c0470ad85..5ab9469441bef 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -5,9 +5,14 @@ */ namespace Magento\Sales\Model\Order\Creditmemo\Total; +/** + * Collects credit memo taxes. + */ class Tax extends AbstractTotal { /** + * Collects credit memo taxes. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @@ -79,18 +84,10 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $totalDiscountTaxCompensation += $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; $baseTotalDiscountTaxCompensation += $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; - $shippingDiscountTaxCompensationAmount = - $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; - $baseShippingDiscountTaxCompensationAmount = - $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; $shippingTaxAmount = $creditmemo->roundPrice($shippingTaxAmount); $baseShippingTaxAmount = $creditmemo->roundPrice($baseShippingTaxAmount, 'base'); $totalDiscountTaxCompensation = $creditmemo->roundPrice($totalDiscountTaxCompensation); $baseTotalDiscountTaxCompensation = $creditmemo->roundPrice($baseTotalDiscountTaxCompensation, 'base'); - $shippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($shippingDiscountTaxCompensationAmount); - $baseShippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($baseShippingDiscountTaxCompensationAmount, 'base'); if ($taxFactor < 1 && $invoice->getShippingTaxAmount() > 0) { $isPartialShippingRefunded = true; } diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php index fd5c015d9db4f..6e12f10f0c679 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +/** + * Collects invoice taxes. + */ class Tax extends AbstractTotal { /** @@ -69,11 +72,24 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } } + $taxDiscountCompensationAmt = $totalDiscountTaxCompensation; + $baseTaxDiscountCompensationAmt = $baseTotalDiscountTaxCompensation; + $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() - + $order->getDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() - + $order->getBaseDiscountTaxCompensationInvoiced(); + if ($this->_canIncludeShipping($invoice)) { $totalTax += $order->getShippingTaxAmount(); $baseTotalTax += $order->getBaseShippingTaxAmount(); $totalDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount(); $baseTotalDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt(); + + $allowedDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount() - + $order->getShippingDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt() - + $order->getBaseShippingDiscountTaxCompensationInvoiced(); + $invoice->setShippingTaxAmount($order->getShippingTaxAmount()); $invoice->setBaseShippingTaxAmount($order->getBaseShippingTaxAmount()); $invoice->setShippingDiscountTaxCompensationAmount($order->getShippingDiscountTaxCompensationAmount()); @@ -81,14 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } $allowedTax = $order->getTaxAmount() - $order->getTaxInvoiced(); $allowedBaseTax = $order->getBaseTaxAmount() - $order->getBaseTaxInvoiced(); - $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() + - $order->getShippingDiscountTaxCompensationAmount() - - $order->getDiscountTaxCompensationInvoiced() - - $order->getShippingDiscountTaxCompensationInvoiced(); - $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() + - $order->getBaseShippingDiscountTaxCompensationAmnt() - - $order->getBaseDiscountTaxCompensationInvoiced() - - $order->getBaseShippingDiscountTaxCompensationInvoiced(); if ($invoice->isLast()) { $totalTax = $allowedTax; @@ -107,8 +115,8 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setTaxAmount($totalTax); $invoice->setBaseTaxAmount($baseTotalTax); - $invoice->setDiscountTaxCompensationAmount($totalDiscountTaxCompensation); - $invoice->setBaseDiscountTaxCompensationAmount($baseTotalDiscountTaxCompensation); + $invoice->setDiscountTaxCompensationAmount($taxDiscountCompensationAmt); + $invoice->setBaseDiscountTaxCompensationAmount($baseTaxDiscountCompensationAmt); $invoice->setGrandTotal($invoice->getGrandTotal() + $totalTax + $totalDiscountTaxCompensation); $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $baseTotalTax + $baseTotalDiscountTaxCompensation); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php index f6dd8f8527a53..82c612c1a781d 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php @@ -35,4 +35,19 @@ public function __construct( ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel); } + + /** + * @inheritdoc + */ + protected function _initSelect() + { + parent::_initSelect(); + + $tableDescription = $this->getConnection()->describeTable($this->getMainTable()); + foreach ($tableDescription as $columnInfo) { + $this->addFilterToMap($columnInfo['COLUMN_NAME'], 'main_table.' . $columnInfo['COLUMN_NAME']); + } + + return $this; + } } diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index 905e9cc4eae1c..1173fa4b7eb5a 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -189,6 +189,8 @@ public function refund( } /** + * Checks if credit memo is available for refund. + * * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo * @return bool * @throws \Magento\Framework\Exception\LocalizedException @@ -211,7 +213,7 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface throw new \Magento\Framework\Exception\LocalizedException( __( 'The most money available to refund is %1.', - $creditmemo->getOrder()->formatBasePrice($baseAvailableRefund) + $creditmemo->getOrder()->formatPriceTxt($baseAvailableRefund) ) ); } @@ -219,8 +221,9 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface } /** - * @return \Magento\Sales\Model\Order\RefundAdapterInterface + * Initializes RefundAdapterInterface dependency. * + * @return \Magento\Sales\Model\Order\RefundAdapterInterface * @deprecated 100.1.3 */ private function getRefundAdapter() @@ -233,8 +236,9 @@ private function getRefundAdapter() } /** - * @return \Magento\Framework\App\ResourceConnection|mixed + * Initializes ResourceConnection dependency. * + * @return \Magento\Framework\App\ResourceConnection|mixed * @deprecated 100.1.3 */ private function getResource() @@ -247,8 +251,9 @@ private function getResource() } /** - * @return \Magento\Sales\Api\OrderRepositoryInterface + * Initializes OrderRepositoryInterface dependency. * + * @return \Magento\Sales\Api\OrderRepositoryInterface * @deprecated 100.1.3 */ private function getOrderRepository() @@ -261,8 +266,9 @@ private function getOrderRepository() } /** - * @return \Magento\Sales\Api\InvoiceRepositoryInterface + * Initializes InvoiceRepositoryInterface dependency. * + * @return \Magento\Sales\Api\InvoiceRepositoryInterface * @deprecated 100.1.3 */ private function getInvoiceRepository() diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml new file mode 100644 index 0000000000000..65a4b512394d8 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!--Filter order grid by order id field--> + <actionGroup name="filterOrderGridById"> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> + <waitForPageLoad stepKey="waitForOrderGridLoad"/> + <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> + <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> + <click selector="{{AdminOrdersGridSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> + </actionGroup> + <actionGroup name="AdminOrdersGridClearFiltersActionGroup"> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToGridOrdersPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.enabledFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 18823b2b0acc2..f9e954548a4d0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -11,7 +11,12 @@ <section name="AdminOrdersGridSection"> <element name="search" type="input" selector="#fulltext"/> <element name="submitSearch" type="button" selector=".//*[@id='container']/div/div[2]/div[1]/div[2]/button"/> - <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> + <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)" timeout="30"/> <element name="createNewOrder" type="button" selector=".page-actions-buttons button#add" timeout="30"/> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> + <element name="idFilter" type="input" selector=".admin__data-grid-filters input[name='increment_id']"/> + <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="enabledFilters" type="block" selector=".admin__data-grid-header .admin__data-grid-filters-current._show"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index eaad9912c6de1..82ac6d9515ce6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -61,6 +61,7 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onAdminOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearch"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php index 2e668f0b0d6f1..68681c6c5a66b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Model\Service; use Magento\Sales\Model\Order; +use Magento\Sales\Api\Data\CreditmemoInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class CreditmemoServiceTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,34 +19,34 @@ class CreditmemoServiceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|MockObject */ protected $creditmemoRepositoryMock; /** - * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|MockObject */ protected $creditmemoCommentRepositoryMock; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\SearchCriteriaBuilder|MockObject */ protected $searchCriteriaBuilderMock; /** - * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\FilterBuilder|MockObject */ protected $filterBuilderMock; /** - * @var \Magento\Sales\Model\Order\CreditmemoNotifier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Model\Order\CreditmemoNotifier|MockObject */ protected $creditmemoNotifierMock; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|MockObject */ - private $priceCurrencyMock; + private $priceCurrency; /** * @var \Magento\Sales\Model\Service\CreditmemoService @@ -79,7 +84,7 @@ protected function setUp() ['setField', 'setValue', 'setConditionType', 'create'] ); $this->creditmemoNotifierMock = $this->createMock(\Magento\Sales\Model\Order\CreditmemoNotifier::class); - $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) ->getMockForAbstractClass(); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -91,7 +96,7 @@ protected function setUp() 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'filterBuilder' => $this->filterBuilderMock, 'creditmemoNotifier' => $this->creditmemoNotifierMock, - 'priceCurrency' => $this->priceCurrencyMock, + 'priceCurrency' => $this->priceCurrency, ] ); } @@ -187,7 +192,7 @@ public function testRefund() $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); - $this->priceCurrencyMock->expects($this->any()) + $this->priceCurrency->expects($this->any()) ->method('round') ->willReturnArgument(0); @@ -259,7 +264,7 @@ public function testRefundPendingCreditMemo() $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); - $this->priceCurrencyMock->expects($this->any()) + $this->priceCurrency->expects($this->any()) ->method('round') ->willReturnArgument(0); @@ -324,27 +329,32 @@ public function testRefundExpectsMoneyAvailableToReturn() $baseGrandTotal = 10; $baseTotalRefunded = 9; $baseTotalPaid = 10; - $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) - ->setMethods(['getId', 'getOrder', 'formatBasePrice']) + /** @var CreditmemoInterface|MockObject $creditMemo */ + $creditMemo = $this->getMockBuilder(CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder']) ->getMockForAbstractClass(); - $creditMemoMock->expects($this->once())->method('getId')->willReturn(null); - $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); - $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); - $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn($baseGrandTotal); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalRefunded')->willReturn($baseTotalRefunded); - $this->priceCurrencyMock->expects($this->exactly(2))->method('round')->withConsecutive( - [$baseTotalRefunded + $baseGrandTotal], - [$baseTotalPaid] - )->willReturnOnConsecutiveCalls( - $baseTotalRefunded + $baseGrandTotal, - $baseTotalPaid - ); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalPaid')->willReturn($baseTotalPaid); + $creditMemo->method('getId') + ->willReturn(null); + /** @var Order|MockObject $order */ + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $creditMemo->method('getOrder') + ->willReturn($order); + $creditMemo->method('getBaseGrandTotal') + ->willReturn($baseGrandTotal); + $order->method('getBaseTotalRefunded') + ->willReturn($baseTotalRefunded); + $this->priceCurrency->method('round') + ->withConsecutive([$baseTotalRefunded + $baseGrandTotal], [$baseTotalPaid]) + ->willReturnOnConsecutiveCalls($baseTotalRefunded + $baseGrandTotal, $baseTotalPaid); + $order->method('getBaseTotalPaid') + ->willReturn($baseTotalPaid); $baseAvailableRefund = $baseTotalPaid - $baseTotalRefunded; - $orderMock->expects($this->once())->method('formatBasePrice')->with( - $baseAvailableRefund - )->willReturn($baseAvailableRefund); - $this->creditmemoService->refund($creditMemoMock, true); + $order->method('formatPriceTxt') + ->with($baseAvailableRefund) + ->willReturn($baseAvailableRefund); + $this->creditmemoService->refund($creditMemo, true); } /** diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php index d2e7cabe473f4..c90894febd44b 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php @@ -25,6 +25,136 @@ protected function _addSpecialAttributes(array &$attributes) $attributes['quote_item_qty'] = __('Quantity in cart'); $attributes['quote_item_price'] = __('Price in cart'); $attributes['quote_item_row_total'] = __('Row total in cart'); + + $attributes['parent::category_ids'] = __('Category (Parent only)'); + $attributes['children::category_ids'] = __('Category (Children Only)'); + } + + /** + * Retrieve attribute + * + * @return string + */ + public function getAttribute(): string + { + $attribute = $this->getData('attribute'); + if (strpos($attribute, '::') !== false) { + list(, $attribute) = explode('::', $attribute); + } + + return $attribute; + } + + /** + * @inheritdoc + */ + public function getAttributeName() + { + $attribute = $this->getAttribute(); + if ($this->getAttributeScope()) { + $attribute = $this->getAttributeScope() . '::' . $attribute; + } + + return $this->getAttributeOption($attribute); + } + + /** + * @inheritdoc + */ + public function loadAttributeOptions() + { + $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode(); + + $attributes = []; + foreach ($productAttributes as $attribute) { + /* @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + if (!$attribute->isAllowedForRuleCondition() + || !$attribute->getDataUsingMethod($this->_isUsedForRuleProperty) + ) { + continue; + } + $frontLabel = $attribute->getFrontendLabel(); + $attributes[$attribute->getAttributeCode()] = $frontLabel; + $attributes['parent::' . $attribute->getAttributeCode()] = $frontLabel . __('(Parent Only)'); + $attributes['children::' . $attribute->getAttributeCode()] = $frontLabel . __('(Children Only)'); + } + + $this->_addSpecialAttributes($attributes); + + asort($attributes); + $this->setAttributeOption($attributes); + + return $this; + } + + /** + * @inheritdoc + */ + public function getAttributeElementHtml() + { + $html = parent::getAttributeElementHtml() . + $this->getAttributeScopeElement()->getHtml(); + + return $html; + } + + /** + * Retrieve form element for scope element + * + * @return \Magento\Framework\Data\Form\Element\AbstractElement + */ + private function getAttributeScopeElement(): \Magento\Framework\Data\Form\Element\AbstractElement + { + return $this->getForm()->addField( + $this->getPrefix() . '__' . $this->getId() . '__attribute_scope', + 'hidden', + [ + 'name' => $this->elementName . '[' . $this->getPrefix() . '][' . $this->getId() . '][attribute_scope]', + 'value' => $this->getAttributeScope(), + 'no_span' => true, + 'class' => 'hidden', + 'data-form-part' => $this->getFormName(), + ] + ); + } + + /** + * Set attribute value + * + * @param string $value + * @return void + */ + public function setAttribute(string $value) + { + if (strpos($value, '::') !== false) { + list($scope, $attribute) = explode('::', $value); + $this->setData('attribute_scope', $scope); + $this->setData('attribute', $attribute); + } else { + $this->setData('attribute', $value); + } + } + + /** + * @inheritdoc + */ + public function loadArray($arr) + { + parent::loadArray($arr); + $this->setAttributeScope($arr['attribute_scope'] ?? null); + + return $this; + } + + /** + * @inheritdoc + */ + public function asArray(array $arrAttributes = []) + { + $out = parent::asArray($arrAttributes); + $out['attribute_scope'] = $this->getAttributeScope(); + + return $out; } /** diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php index b5ac02e67b1e1..2277240eb8aad 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php @@ -8,6 +8,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; /** + * Combine conditions for product. + * * @api * @since 100.0.2 */ @@ -85,4 +87,76 @@ public function collectValidatedAttributes($productCollection) } return $this; } + + /** + * @inheritdoc + */ + protected function _isValid($entity) + { + if (!$this->getConditions()) { + return true; + } + + $all = $this->getAggregator() === 'all'; + $true = (bool)$this->getValue(); + + foreach ($this->getConditions() as $cond) { + if ($entity instanceof \Magento\Framework\Model\AbstractModel) { + $validated = $this->validateEntity($entity, $cond); + } else { + $validated = $cond->validateByEntityId($entity); + } + if ($all && $validated !== $true) { + return false; + } elseif (!$all && $validated === $true) { + return true; + } + } + + return $all ? true : false; + } + + /** + * Validate entity. + * + * @param \Magento\Framework\Model\AbstractModel $entity + * @param mixed $cond + * @return bool + */ + private function validateEntity(\Magento\Framework\Model\AbstractModel $entity, $cond): bool + { + $true = (bool)$this->getValue(); + $validated = !$true; + foreach ($this->retrieveValidateEntities($entity, $cond->getAttributeScope()) as $validateEntity) { + $validated = $cond->validate($validateEntity); + if ($validated === $true) { + break; + } + } + + return $validated; + } + + /** + * Retrieve entities for validation by attribute scope + * + * @param \Magento\Framework\Model\AbstractModel $entity + * @param string|null $attributeScope + * @return \Magento\Framework\Model\AbstractModel[] + */ + private function retrieveValidateEntities( + \Magento\Framework\Model\AbstractModel $entity, + $attributeScope + ): array { + if ($attributeScope === 'parent') { + $validateEntities = [$entity]; + } elseif ($attributeScope === 'children') { + $validateEntities = $entity->getChildren() ?: [$entity]; + } else { + $validateEntities = $entity->getChildren() ?: []; + $validateEntities[] = $entity; + } + + return $validateEntities; + } } diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 65f69e582bc07..35ad7f96bd18d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -45,6 +45,15 @@ <data key="apply">Percent of product price discount</data> <data key="discountAmount">50</data> </entity> + <entity name="SalesRuleWithSkuInActions" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN'</data> + <data key="is_active">true</data> + <data key="coupon_type">Specific Coupon</data> + <data key="coupon_code">ABCD</data> + <data key="discount_amount">70</data> + </entity> <entity name="SalesRuleSpecificCoupon" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> <data key="description">Sales Rule Descritpion</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.xml new file mode 100644 index 0000000000000..83ec6095d57ee --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/CheckoutCartPage.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CheckoutCartPage" url="/checkout/cart" area="storefront" module="Magento_Checkout"> + <section name="StorefrontDiscountSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index 8839a9da0ba14..14584acd03376 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -32,6 +32,11 @@ <element name="discountAmount" type="input" selector="input[name='discount_amount']"/> <element name="discountStep" type="input" selector="input[name='discount_step']"/> <element name="freeShipping" type="select" selector="select[name='simple_free_shipping']"/> + <element name="conditions" type="button" selector=".rule-param.rule-param-new-child > a"/> + <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{arg}}']" parameterized="true"/> + <element name="operator" type="select" selector="select[name*='[operator]']"/> + <element name="childAttribute" type="select" selector="select[name*='new_child']"/> + <element name="optionInput" type="input" selector="ul[class*='rule-param-children'] input[name*='[value]']"/> <!-- Manage Coupon Codes sub-form --> <element name="manageCouponCodesHeader" type="button" selector="div[data-index='manage_coupon_codes']" timeout="30"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml new file mode 100644 index 0000000000000..2ae50489b6d12 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontDiscountSection"> + <element name="discountTab" type="button" selector="#block-discount"/> + <element name="couponInput" type="input" selector="#coupon_code"/> + <element name="applyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml new file mode 100644 index 0000000000000..f7010822f5f43 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="CartPriceRuleForConfigurableProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="MAGETWO-86098 - Cart Price Rule for configurable products"/> + <title value="Checking Cart Price Rule for configurable products"/> + <description value="Checking Cart Price Rule for configurable products"/> + <severity value="BLOCKER"/> + <testCaseId value="MAGETWO-95121"/> + <group value="sales_rule"/> + </annotations> + + <before> + <!--Login to Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create a simple product and give it the attribute with option --> + <createData entity="SimpleOption" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="SimpleOption" stepKey="createConfigChildProduct2"> + <field key="sku">SimpleTwoOption</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!--Set attribute sku property Use for Promo Rule Conditions = Yes and save attribute--> + <actionGroup ref="navigateToProductAttribute" stepKey="goToProductAttributeSkuPage"> + <argument name="attributeCode" value="sku"/> + </actionGroup> + <click selector="{{AdminEditAttributeStorefrontPropertiesSection.storeFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <scrollTo selector="{{AdminEditAttributeStorefrontPropertiesSection.storeFrontPropertiesTab}}" stepKey="scrollToUseForPromoRuleConditions"/> + <selectOption selector="{{AdminEditAttributeStorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="Yes" stepKey="changeAttributeProperty"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeIsSaved"/> + + <!-- Create cart price rule --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SalesRuleWithSkuInActions.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{SalesRuleWithSkuInActions.websites}}" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="{{SalesRuleWithSkuInActions.coupon_type}}" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{SalesRuleWithSkuInActions.coupon_code}}" stepKey="fillCouponCOde"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{SalesRuleWithSkuInActions.discount_amount}}" stepKey="fillDiscountAmount"/> + <scrollTo selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="scrollToApplyRuleForConditions"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="addConditionForRule"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="SKU(Children Only)" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickToChooseCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.operator}}" userInput="is not one of" stepKey="selectOperator"/> + <waitForPageLoad stepKey="waitForOperatorOpened1"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption"/> + <waitForPageLoad stepKey="waitForConditionOpened2"/> + <fillField selector="{{AdminCartPriceRulesFormSection.optionInput}}" userInput="$$createConfigChildProduct1.sku$$" stepKey="selectOption"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </before> + + <after> + <!--Remove SalesRule--> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> + <argument name="ruleName" value="{{SalesRuleWithSkuInActions.name}}"/> + </actionGroup> + <!--Return default value to attribute sku--> + <actionGroup ref="navigateToProductAttribute" stepKey="goToProductAttributeSkuPage"> + <argument name="attributeCode" value="sku"/> + </actionGroup> + <click selector="{{AdminEditAttributeStorefrontPropertiesSection.storeFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab1"/> + <scrollTo selector="{{AdminEditAttributeStorefrontPropertiesSection.storeFrontPropertiesTab}}" stepKey="scrollToUseForPromoRuleConditions"/> + <selectOption selector="{{AdminEditAttributeStorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="No" stepKey="changeAttributeProperty"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeIsSaved"/> + + <!--Remove configurable product and it's children--> + <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"/> + <!--Logout from Admin--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add the configureble product with first option to the cart --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.sku$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + userInput="$$getConfigAttributeOption1.label$$" stepKey="selectOption1"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + <!--Add the configureble product with second option to the cart --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.sku$$)}}" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + userInput="$$getConfigAttributeOption2.label$$" stepKey="selectOption2"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> + <waitForPageLoad stepKey="waitForAddToCart1"/> + + <!--View and edit cart--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="storefrontOpenCartFromMinicart"/> + <scrollTo selector="{{StorefrontDiscountSection.discountTab}}" stepKey="scrollToDiscountTab"/> + <click selector="{{StorefrontDiscountSection.discountTab}}" stepKey="openDiscountTab" /> + <fillField selector="{{StorefrontDiscountSection.couponInput}}" userInput="{{SalesRuleWithSkuInActions.coupon_code}}" stepKey="fillCouponCode" /> + <click selector="{{StorefrontDiscountSection.applyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForPageLoaded1"/> + <see userInput="You used coupon code" stepKey="assertText"/> + <!--Verify values--> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.itemDiscount}}" stepKey="getDiscount"/> + <grabTextFrom selector="{{StorefrontCheckoutCartSummarySection.subtotal}}" stepKey="getSubtotal"/> + <assertEquals stepKey="checkDescount"> + <expectedResult type="string">-$7.00</expectedResult> + <actualResult type="variable">$getDiscount</actualResult> + </assertEquals> + <assertEquals stepKey="checkSubtotal"> + <expectedResult type="string">$20.00</expectedResult> + <actualResult type="variable">$getSubtotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index b217c4bdf8c8b..929cdc1e1ec7e 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -295,8 +295,8 @@ public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator ->method('getProduct') ->willReturn($product); - $this->model->setAttribute('quote_item_price') - ->setOperator($operator); + $this->model->setAttribute('quote_item_price'); + $this->model->setData('operator', $operator); $this->assertEquals($isValid, $this->model->setValue($conditionValue)->validate($item)); } diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 2b25e0efab84a..4922a9f365ced 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -17,13 +17,13 @@ <div class="secondary"> <button type="button" id="btn-remove<%- data._index_ %>" class="action remove" title="<?= $block->escapeHtmlAttr(__('Remove Recipent')) ?>"> - <span><?= $block->escapeJs($block->escapeHtml(__('Remove'))) ?></span> + <span><?= $block->escapeHtml(__('Remove')) ?></span> </button> </div> </div> <fieldset class="fieldset"> <div class="field name required"> - <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Name'))) ?></span></label> + <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> <input name="recipients[name][<%- data._index_ %>]" type="text" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" id="recipients-name<%- data._index_ %>" data-validate="{required:true}"/> @@ -31,7 +31,7 @@ </div> <div class="field email required"> - <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Email'))) ?></span></label> + <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" diff --git a/app/code/Magento/Store/Model/StoreManager.php b/app/code/Magento/Store/Model/StoreManager.php index a3423b96c3fe4..c47750c0fd8d0 100644 --- a/app/code/Magento/Store/Model/StoreManager.php +++ b/app/code/Magento/Store/Model/StoreManager.php @@ -234,11 +234,11 @@ public function getWebsites($withDefault = false, $codeKey = false) public function reinitStores() { $this->currentStoreId = null; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [StoreResolver::CACHE_TAG, Store::CACHE_TAG]); - $this->scopeConfig->clean(); $this->storeRepository->clean(); $this->websiteRepository->clean(); $this->groupRepository->clean(); + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [StoreResolver::CACHE_TAG, Store::CACHE_TAG]); + $this->scopeConfig->clean(); } /** diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml index 714acf8050f6f..22c5d747748cd 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -5,7 +5,18 @@ * See COPYING.txt for license details. */ --> -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultWebsite" type="group"> + <data key="name">Main Website</data> + <data key="code">base</data> + <data key="sort_order">1</data> + </entity> + <entity name="CustomWebSite" type="website"> + <data key="name" unique="suffix">website</data> + <data key="code" unique="suffix">website</data> + <data key="sort_order">2</data> + </entity> <entity name="SecondWebsite" type="website"> <data key="name" unique="suffix">Second Website </data> <data key="code" unique="suffix">second_website_</data> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml index f8c55d6426982..f19da436a60c4 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml @@ -21,7 +21,8 @@ "numberToShow": <?= /* @escapeNotVerified */ $block->getNumberSwatchesPerProduct(); ?>, "jsonConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig(); ?>, "jsonSwatchConfig": <?= /* @escapeNotVerified */ $block->getJsonSwatchConfig(); ?>, - "mediaCallback": "<?= /* @escapeNotVerified */ $block->getMediaCallback() ?>" + "mediaCallback": "<?= /* @escapeNotVerified */ $block->getMediaCallback() ?>", + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> } } } diff --git a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css index dc7b1abd65bf9..67b9fffcf1282 100644 --- a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css @@ -35,7 +35,7 @@ /*width: 30px;*/ padding: 1px 2px; min-width: 30px; - max-width: 90px; + max-width: 100%; height: 20px; float: left; margin: 0 10px 5px 0; diff --git a/app/code/Magento/Theme/etc/config.xml b/app/code/Magento/Theme/etc/config.xml index a6984b449d944..b44691c0df963 100644 --- a/app/code/Magento/Theme/etc/config.xml +++ b/app/code/Magento/Theme/etc/config.xml @@ -46,6 +46,7 @@ Disallow: /*SID= </header> <footer translate="copyright"> <copyright>Copyright © 2013-present Magento, Inc. All rights reserved.</copyright> + <report_bugs>1</report_bugs> </footer> </design> <theme> diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 21e1736d01bf1..a4bfaa18a2994 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -213,6 +213,10 @@ <item name="path" xsi:type="string">design/footer/absolute_footer</item> <item name="fieldset" xsi:type="string">other_settings/footer</item> </item> + <item name="footer_report_bugs" xsi:type="array"> + <item name="path" xsi:type="string">design/footer/report_bugs</item> + <item name="fieldset" xsi:type="string">other_settings/footer</item> + </item> <item name="default_robots" xsi:type="array"> <item name="path" xsi:type="string">design/search_engine_robots/default_robots</item> <item name="fieldset" xsi:type="string">other_settings/search_engine_robots</item> diff --git a/app/code/Magento/Theme/i18n/en_US.csv b/app/code/Magento/Theme/i18n/en_US.csv index b454b7222bd1c..e18798f4bf165 100644 --- a/app/code/Magento/Theme/i18n/en_US.csv +++ b/app/code/Magento/Theme/i18n/en_US.csv @@ -185,3 +185,7 @@ Settings,Settings "2 columns with left bar","2 columns with left bar" "2 columns with right bar","2 columns with right bar" "3 columns","3 columns" +ID,ID +View,View +Action,Action +"Display Report Bugs Link","Display Report Bugs Link" 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 ac734699e4d71..3dcc265147cf2 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 @@ -234,6 +234,20 @@ <dataScope>footer_copyright</dataScope> </settings> </field> + <field name="footer_report_bugs" formElement="select"> + <settings> + <dataType>text</dataType> + <label translate="true">Display Report Bugs Link</label> + <dataScope>footer_report_bugs</dataScope> + </settings> + <formElements> + <select> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + </settings> + </select> + </formElements> + </field> </fieldset> <fieldset name="search_engine_robots" sortOrder="120"> <settings> diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 716341f5a64a4..c84222be19c3c 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -119,7 +119,7 @@ </arguments> </block> <block class="Magento\Theme\Block\Html\Footer" name="copyright" template="Magento_Theme::html/copyright.phtml"/> - <block class="Magento\Framework\View\Element\Template" name="report.bugs" template="Magento_Theme::html/bugreport.phtml" /> + <block class="Magento\Framework\View\Element\Template" name="report.bugs" template="Magento_Theme::html/bugreport.phtml" ifconfig="design/footer/report_bugs"/> </container> </referenceContainer> <referenceContainer name="before.body.end"> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml index 2c65f4d548c31..8fbd5342b24c4 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridFilterActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Search grid with keyword search--> <actionGroup name="searchAdminDataGridByKeyword"> <arguments> @@ -32,4 +32,17 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> </actionGroup> + + <actionGroup name="AdminGridFilterSearchResultsByInput"> + <arguments> + <argument name="inputName" type="string" defaultValue="name"/> + <argument name="value" type="string"/> + </arguments> + <!--Clear all filters in grid if they present--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearTheFiltersIfPresent"/> + + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{value}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('inputName')}}" stepKey="fillCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOnApplyFilters"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml index e5766300b0e87..c23909dfdbe24 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml @@ -19,6 +19,7 @@ <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="removeFilter" type="button" selector=".admin__data-grid-filters-current._show .action-remove" timeout="30"/> <!--Grid view bookmarks--> <element name="bookmarkToggle" type="button" selector="div.admin__data-grid-action-bookmarks button[data-bind='toggleCollapsible']" timeout="30"/> <element name="bookmarkOption" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Ui/i18n/en_US.csv b/app/code/Magento/Ui/i18n/en_US.csv index cff52a3fd6fed..981d444b31d53 100644 --- a/app/code/Magento/Ui/i18n/en_US.csv +++ b/app/code/Magento/Ui/i18n/en_US.csv @@ -58,6 +58,7 @@ Keyword,Keyword "Letters, numbers, spaces or underscores only please","Letters, numbers, spaces or underscores only please" "Letters only please","Letters only please" "No white space please","No white space please" +"No marginal white space please","No marginal white space please" "Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx","Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx" "A positive or negative non-decimal number please","A positive or negative non-decimal number please" "The specified vehicle identification number (VIN) is invalid.","The specified vehicle identification number (VIN) is invalid." diff --git a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js index fe312738469e7..ac1de4631e908 100644 --- a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js +++ b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js @@ -224,7 +224,7 @@ define([ */ build: function (parent, node, name) { var defaults = parent && parent.childDefaults || {}, - children = node.children, + children = this.filterDisabledChildren(node.children), type = getNodeType(parent, node), dataScope = getDataScope(parent, node), component, @@ -294,6 +294,35 @@ define([ return node; }, + /** + * Filter out all disabled components. + * + * @param {Object} children + * @returns {*} + */ + filterDisabledChildren: function (children) { + var cIds; + + //cleanup children config.componentDisabled = true + if (children && typeof children === 'object') { + cIds = Object.keys(children); + + if (cIds) { + _.each(cIds, function (cId) { + if (typeof children[cId] === 'object' && + children[cId].hasOwnProperty('config') && + typeof children[cId].config === 'object' && + children[cId].config.hasOwnProperty('componentDisabled') && + children[cId].config.componentDisabled === true) { + delete children[cId]; + } + }); + } + } + + return children; + }, + /** * Init component. * diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index eea199176681c..8e59f9d290906 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -115,6 +115,12 @@ define([ }, $.mage.__('No white space please') ], + 'no-marginal-whitespace': [ + function (value) { + return !/^\s+|\s+$/i.test(value); + }, + $.mage.__('No marginal white space please') + ], 'zip-range': [ function (value) { return utils.isEmpty(value) || /^90[2-5]-\d{2}-\d{4}$/.test(value); diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal-component.js b/app/code/Magento/Ui/view/base/web/js/modal/modal-component.js index e7b2444dad95c..f82d24e82736a 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal-component.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal-component.js @@ -93,7 +93,7 @@ define([ * @returns {Object} Chainable. */ initModalEvents: function () { - this.options.keyEventHandlers.escapeKey = this.options.outerClickHandler = this[this.onCancel].bind(this); + this.options.keyEventHandlers.escapeKey = this[this.onCancel].bind(this); return this; }, diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index 147a18c08c54c..884d6f46defcd 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -424,8 +424,7 @@ define([ * Creates overlay, append it to wrapper, set previous click event on overlay. */ _createOverlay: function () { - var events, - outerClickHandler = this.options.outerClickHandler || this.closeModal; + var events; this.overlay = $('.' + this.options.overlayClass); @@ -437,7 +436,6 @@ define([ } events = $._data(this.overlay.get(0), 'events'); events ? this.prevOverlayHandler = events.click[0].handler : false; - this.options.clickableOverlay ? this.overlay.unbind().on('click', outerClickHandler) : false; }, /** diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.xml new file mode 100644 index 0000000000000..9524e41dc7c9e --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminUrlRewriteIndexPage" url="admin/url_rewrite/index/" area="admin" module="Magento_UrlRewrite"> + <section name="AdminUrlRewriteIndexSection"/> + </page> +</pages> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml new file mode 100644 index 0000000000000..455748a0da534 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminUrlRewriteIndexSection"> + <element name="requestPathFilter" type="input" selector="#urlrewriteGrid_filter_request_path"/> + <element name="requestPathColumnValue" type="text" selector="//*[@id='urlrewriteGrid']//tbody//td[@data-column='request_path' and normalize-space(.)='{{columnValue}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml new file mode 100644 index 0000000000000..363e6491a25f0 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminUrlRewritesForProductInAnchorCategoriesTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Url-rewrites for product in anchor categories"/> + <title value="Url-rewrites for product in anchor categories"/> + <description value="For a product with category that has parent anchor categories, the rewrites is created when the category/product is saved."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-76098"/> + <group value="urlRewrite"/> + </annotations> + + <!-- Preconditions--> + <!-- Create 3 categories --> + <before> + <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> + <requiredEntity createDataKey="simpleSubCategory1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory3"> + <requiredEntity createDataKey="simpleSubCategory2"/> + </createData> + <!-- Create Simple product 1 and assign it to Category 3 --> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory3"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Steps --> + <!-- 1. Log in to Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- 2. Open Marketing - SEO & Search - URL Rewrites --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="inputProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$simpleSubCategory2.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$simpleSubCategory2.custom_attributes[url_key]$$/$$simpleSubCategory3.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeValue4"/> + + <!-- 3. Edit Category 1 for DEFAULT Store View: --> + <actionGroup ref="switchCategoryStoreView" stepKey="switchStoreView"> + <argument name="store" value="_defaultStore.name"/> + <argument name="catName" value="$$simpleSubCategory1.name$$"/> + </actionGroup> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> + <uncheckOption selector="{{AdminCategorySEOSection.urlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$$simpleSubCategory1.custom_attributes[url_key]$$-new" stepKey="changeURLKey"/> + <checkOption selector="{{AdminCategorySEOSection.urlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> + + <!-- 4. Save Category 1 --> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> + + <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="inputProductName2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$simpleSubCategory2.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$/$$simpleSubCategory2.custom_attributes[url_key]$$/$$simpleSubCategory3.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$-new/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$-new/$$simpleSubCategory2.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue6"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('$$simpleSubCategory1.custom_attributes[url_key]$$-new/$$simpleSubCategory2.custom_attributes[url_key]$$/$$simpleSubCategory3.custom_attributes[url_key]$$/$$createSimpleProduct.custom_attributes[url_key]$$.html')}}" stepKey="seeInListValue7"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Setup/UpgradeSchema.php b/app/code/Magento/Wishlist/Setup/UpgradeSchema.php new file mode 100644 index 0000000000000..20297da771a8e --- /dev/null +++ b/app/code/Magento/Wishlist/Setup/UpgradeSchema.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Setup; + +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; + +/** + * Upgrade Schema + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * @inheritdoc + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + $connection = $setup->getConnection(); + if (version_compare($context->getVersion(), '2.0.2', '<')) { + $connection->addForeignKey( + $connection->getForeignKeyName( + $setup->getTable('wishlist_item_option'), + 'product_id', + $setup->getTable('catalog_product_entity'), + 'entity_id' + ), + $setup->getTable('wishlist_item_option'), + 'product_id', + $setup->getTable('catalog_product_entity'), + 'entity_id', + AdapterInterface::FK_ACTION_CASCADE, + true + ); + } + $setup->endSetup(); + } +} diff --git a/app/code/Magento/Wishlist/etc/module.xml b/app/code/Magento/Wishlist/etc/module.xml index e7626f504e1f1..ade606be9e086 100644 --- a/app/code/Magento/Wishlist/etc/module.xml +++ b/app/code/Magento/Wishlist/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Wishlist" setup_version="2.0.1"> + <module name="Magento_Wishlist" setup_version="2.0.2"> <sequence> <module name="Magento_Customer"/> <module name="Magento_Catalog"/> diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less index 3265c8b2fc387..8fcb0f94fff4e 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less @@ -151,7 +151,7 @@ background-color: transparent; border: 1px solid transparent; font-size: @search-global-input__font-size; - height: @search-global-input__height; + height: @search-global-input__height + .2; padding: @search-global-input__padding-top @search-global-input__padding-side @search-global-input__padding-bottom; position: absolute; right: 0; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_calendar-temp.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_calendar-temp.less index 416fb2a3071a5..032df8cf4373a 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_calendar-temp.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_calendar-temp.less @@ -43,7 +43,7 @@ height: @action__height; margin-left: -@action__height; overflow: hidden; - position: relative; + position: absolute; vertical-align: top; z-index: 1; diff --git a/composer.json b/composer.json index 9f38adf3eac9f..5a35f7c94a209 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "zendframework/zend-db": "^2.8.2", "zendframework/zend-captcha": "^2.7.1", "zendframework/zend-session": "^2.7.3", + "zendframework/zend-mail": "^2.9.0", "magento/zendframework1": "~1.13.0", "colinmollenhour/credis": "1.8.2", "colinmollenhour/php-redis-session-abstract": "1.3.4", diff --git a/composer.lock b/composer.lock index 64a11d27b5295..c765d467dfd0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "78c402f8214c96270f62f23eee80a6f1", + "content-hash": "c9603f6f68dc6d6cc1ca6b6be242da74", "packages": [ { "name": "braintree/braintree_php", @@ -1436,16 +1436,16 @@ }, { "name": "symfony/console", - "version": "v2.8.45", + "version": "v2.8.46", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0c1fcbb9afb5cff992c982ff99c0434f0146dcfc" + "reference": "aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0c1fcbb9afb5cff992c982ff99c0434f0146dcfc", - "reference": "0c1fcbb9afb5cff992c982ff99c0434f0146dcfc", + "url": "https://api.github.com/repos/symfony/console/zipball/aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789", + "reference": "aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789", "shasum": "" }, "require": { @@ -1493,7 +1493,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:13:39+00:00" + "time": "2018-09-30T03:33:07+00:00" }, { "name": "symfony/debug", @@ -1554,7 +1554,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.45", + "version": "v2.8.46", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -1614,16 +1614,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c" + "reference": "d69930fc337d767607267d57c20a7403d0a822a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", - "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d69930fc337d767607267d57c20a7403d0a822a4", + "reference": "d69930fc337d767607267d57c20a7403d0a822a4", "shasum": "" }, "require": { @@ -1660,20 +1660,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-08-10T07:29:05+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/finder", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a" + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8a84fcb207451df0013b2c74cbbf1b62d47b999a", - "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a", + "url": "https://api.github.com/repos/symfony/finder/zipball/54ba444dddc5bd5708a34bd095ea67c6eb54644d", + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d", "shasum": "" }, "require": { @@ -1709,7 +1709,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-03T08:46:40+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1830,16 +1830,16 @@ }, { "name": "symfony/process", - "version": "v2.8.45", + "version": "v2.8.46", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4be278e19064c3492095de50c9e375caae569ae1" + "reference": "f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4be278e19064c3492095de50c9e375caae569ae1", - "reference": "4be278e19064c3492095de50c9e375caae569ae1", + "url": "https://api.github.com/repos/symfony/process/zipball/f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6", + "reference": "f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6", "shasum": "" }, "require": { @@ -1875,7 +1875,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-08-03T09:45:57+00:00" + "time": "2018-09-06T17:11:15+00:00" }, { "name": "tedivm/jshrink", @@ -1923,6 +1923,52 @@ ], "time": "2018-09-16T00:02:51+00:00" }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "time": "2016-11-16T10:37:54+00:00" + }, { "name": "tubalmartin/cssmin", "version": "v4.1.1", @@ -3053,6 +3099,68 @@ ], "time": "2018-04-09T21:59:51+00:00" }, + { + "name": "zendframework/zend-mail", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-mail.git", + "reference": "d7beb63d5f7144a21ac100072c453e63860cdab8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-mail/zipball/d7beb63d5f7144a21ac100072c453e63860cdab8", + "reference": "d7beb63d5f7144a21ac100072c453e63860cdab8", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.6 || ^7.0", + "true/punycode": "^2.1", + "zendframework/zend-loader": "^2.5", + "zendframework/zend-mime": "^2.5", + "zendframework/zend-stdlib": "^2.7 || ^3.0", + "zendframework/zend-validator": "^2.10.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-config": "^2.6", + "zendframework/zend-crypt": "^2.6 || ^3.0", + "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1" + }, + "suggest": { + "zendframework/zend-crypt": "Crammd5 support in SMTP Auth", + "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1 when using SMTP to deliver messages" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" + }, + "zf": { + "component": "Zend\\Mail", + "config-provider": "Zend\\Mail\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\Mail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", + "keywords": [ + "ZendFramework", + "mail", + "zf" + ], + "time": "2018-06-07T13:37:07+00:00" + }, { "name": "zendframework/zend-math", "version": "2.7.0", @@ -3103,6 +3211,57 @@ ], "time": "2016-04-07T16:29:53+00:00" }, + { + "name": "zendframework/zend-mime", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-mime.git", + "reference": "52ae5fa9f12845cae749271034a2d594f0e4c6f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-mime/zipball/52ae5fa9f12845cae749271034a2d594f0e4c6f2", + "reference": "52ae5fa9f12845cae749271034a2d594f0e4c6f2", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.21 || ^6.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-mail": "^2.6" + }, + "suggest": { + "zendframework/zend-mail": "Zend\\Mail component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev", + "dev-develop": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Mime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create and parse MIME messages and parts", + "homepage": "https://github.com/zendframework/zend-mime", + "keywords": [ + "ZendFramework", + "mime", + "zf" + ], + "time": "2018-05-14T19:02:50+00:00" + }, { "name": "zendframework/zend-modulemanager", "version": "2.8.2", @@ -7637,7 +7796,7 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -7694,16 +7853,16 @@ }, { "name": "symfony/config", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7b08223b7f6abd859651c56bcabf900d1627d085" + "reference": "e5389132dc6320682de3643091121c048ff796b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7b08223b7f6abd859651c56bcabf900d1627d085", - "reference": "7b08223b7f6abd859651c56bcabf900d1627d085", + "url": "https://api.github.com/repos/symfony/config/zipball/e5389132dc6320682de3643091121c048ff796b3", + "reference": "e5389132dc6320682de3643091121c048ff796b3", "shasum": "" }, "require": { @@ -7754,20 +7913,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-09-08T13:15:14+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416" + "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/edda5a6155000ff8c3a3f85ee5c421af93cca416", - "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/3503415d4aafabc31cd08c3a4ebac7f43fde8feb", + "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb", "shasum": "" }, "require": { @@ -7807,7 +7966,7 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:06:28+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/dependency-injection", @@ -7881,16 +8040,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "452bfc854b60134438e3824b159b0d24a5892331" + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/452bfc854b60134438e3824b159b0d24a5892331", - "reference": "452bfc854b60134438e3824b159b0d24a5892331", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c705bee03ade5b47c087807dd9ffaaec8dda2722", + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722", "shasum": "" }, "require": { @@ -7934,20 +8093,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-07-26T10:03:52+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "2fb33cb6eefe6e790e4023f7c534a9e4214252fc" + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2fb33cb6eefe6e790e4023f7c534a9e4214252fc", - "reference": "2fb33cb6eefe6e790e4023f7c534a9e4214252fc", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a4498236ade473c52b92d509303e5fd1b211ab1", + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1", "shasum": "" }, "require": { @@ -7988,20 +8147,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-08-27T17:45:33+00:00" + "time": "2018-10-03T08:48:18+00:00" }, { "name": "symfony/options-resolver", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "6debc476953a45969ab39afe8dee0b825f356dc7" + "reference": "1cf7d8e704a9cc4164c92e430f2dfa3e6983661d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/6debc476953a45969ab39afe8dee0b825f356dc7", - "reference": "6debc476953a45969ab39afe8dee0b825f356dc7", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/1cf7d8e704a9cc4164c92e430f2dfa3e6983661d", + "reference": "1cf7d8e704a9cc4164c92e430f2dfa3e6983661d", "shasum": "" }, "require": { @@ -8042,7 +8201,7 @@ "configuration", "options" ], - "time": "2018-07-26T08:45:46+00:00" + "time": "2018-09-17T17:29:18+00:00" }, { "name": "symfony/polyfill-php54", @@ -8274,16 +8433,16 @@ }, { "name": "symfony/stopwatch", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "deda2765e8dab2fc38492e926ea690f2a681f59d" + "reference": "05e52a39de52ba690aebaed462b2bc8a9649f0a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/deda2765e8dab2fc38492e926ea690f2a681f59d", - "reference": "deda2765e8dab2fc38492e926ea690f2a681f59d", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/05e52a39de52ba690aebaed462b2bc8a9649f0a4", + "reference": "05e52a39de52ba690aebaed462b2bc8a9649f0a4", "shasum": "" }, "require": { @@ -8319,7 +8478,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-07-26T10:03:52+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/yaml", diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 880af262125d0..715f646b533ab 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -920,6 +920,7 @@ public function testGetList() $this->assertTrue(count($response['items']) > 0); $this->assertNotNull($response['items'][0]['sku']); + $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']); $this->assertEquals('simple', $response['items'][0]['sku']); $index = null; @@ -1018,6 +1019,7 @@ public function testGetListWithFilteringByWebsite() $this->assertTrue(count($response['items']) == 1); $this->assertTrue(isset($response['items'][0]['sku'])); $this->assertEquals('simple-2', $response['items'][0]['sku']); + $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']); } /** @@ -1164,6 +1166,9 @@ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() $this->assertEquals(3, $searchResult['total_count']); $this->assertEquals(1, count($searchResult['items'])); $this->assertEquals('search_product_4', $searchResult['items'][0][ProductInterface::SKU]); + $this->assertNotNull( + $searchResult['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids'] + ); } /** diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/Block/System/Config/AnalyticsForm.php b/dev/tests/functional/tests/app/Magento/Analytics/Test/Block/System/Config/AnalyticsForm.php index 07b62a9518ae4..bf1f55915108b 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/Block/System/Config/AnalyticsForm.php +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/Block/System/Config/AnalyticsForm.php @@ -95,12 +95,17 @@ public function getAnalyticsStatus() /** * @param string $vertical - * @return array|string + * @return $this */ public function setAnalyticsVertical($vertical) { - return $this->_rootElement->find($this->analyticsVertical, Locator::SELECTOR_CSS, 'select') - ->setValue($vertical); + + $element = $this->_rootElement->find($this->analyticsVertical, Locator::SELECTOR_CSS, 'select'); + if ($element->isVisible()) { + $element->setValue($vertical); + } + + return $this; } /** diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsDisabled.php b/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsDisabled.php index 0f65835a32aa7..b061a5d644484 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsDisabled.php +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsDisabled.php @@ -34,6 +34,10 @@ public function processAssert(ConfigAnalytics $configAnalytics, OpenAnalyticsCon 'Subscription status: Disabled', 'Magento Advanced Reporting service subscription status is not disabled.' ); + \PHPUnit_Framework_Assert::assertFalse( + (bool)$configAnalytics->getAnalyticsForm()->getAnalyticsVerticalScope(), + 'Industry Data is visible.' + ); } /** diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsEnabled.php b/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsEnabled.php index 8fd04e06b14bb..46d5d8e2d5f78 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsEnabled.php +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/Constraint/AssertConfigAnalyticsEnabled.php @@ -29,12 +29,15 @@ public function processAssert(ConfigAnalytics $configAnalytics, OpenAnalyticsCon (bool)$configAnalytics->getAnalyticsForm()->isAnalyticsEnabled(), 'Magento Advanced Reporting service is not enabled.' ); - \PHPUnit_Framework_Assert::assertEquals( $configAnalytics->getAnalyticsForm()->getAnalyticsStatus(), 'Subscription status: Pending', 'Magento Advanced Reporting service subscription status is not pending.' ); + \PHPUnit_Framework_Assert::assertTrue( + (bool)$configAnalytics->getAnalyticsForm()->getAnalyticsVerticalScope(), + 'Industry Data is not visible.' + ); } /** diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php index 8828f31f9965a..f71c55be8c9fc 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php @@ -8,6 +8,7 @@ use Magento\Bundle\Test\Fixture\BundleProduct; use Magento\Catalog\Test\Page\Product\CatalogProductView; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Constraint\AbstractAssertForm; @@ -17,6 +18,8 @@ */ class AssertBundleProductOnConfigureCartPage extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Check bundle product options correctly displayed on cart configuration page. * @@ -28,6 +31,8 @@ class AssertBundleProductOnConfigureCartPage extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart, CatalogProductView $catalogProductView) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); foreach ($cart->getItems() as $key => $item) { diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php index d1a6ce2d07f2b..86624001756b0 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php @@ -113,6 +113,13 @@ class Cart extends Block */ private $popupWindowContent = '#main'; + /** + * Locator for page with ajax loading state. + * + * @var string + */ + private $ajaxLoading = 'body.ajax-loading'; + /** * Wait for PayPal page is loaded. * @@ -295,4 +302,14 @@ public function waitForCheckoutButton() { $this->waitForElementVisible($this->inContextPaypalCheckoutButton); } + + /** + * Wait loading. + * + * @return void + */ + public function waitForLoader() + { + $this->waitForElementNotVisible($this->ajaxLoading); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php index 10299486a08ce..3d293700db8c9 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php @@ -72,6 +72,13 @@ class Shipping extends Form */ protected $commonShippingPriceSelector = '.totals.shipping .price'; + /** + * Estimate shipping and tax form locator. + * + * @var string + */ + private $estimateShippingForm = '#shipping-zip-form'; + /** * Open estimate shipping and tax form. * @@ -250,4 +257,51 @@ public function waitForCommonShippingPriceBlock() { $this->waitForElementVisible($this->commonShippingPriceSelector, Locator::SELECTOR_CSS); } + + /** + * Wait until estimation form to appear. + * + * @return void + */ + public function waitForEstimateShippingAndTaxForm() + { + $browser = $this->browser; + $selector = $this->estimateShippingForm; + + $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isPresent() ? true : null; + } + ); + } + + /** + * Wait for shipping method form. + * + * @return void + */ + public function waitForShippingMethodForm() + { + $browser = $this->browser; + $selector = $this->shippingMethodForm; + + $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isPresent() ? true : null; + } + ); + } + + /** + * Wait for summary block to be loaded. + * + * @return void + */ + public function waitForSummaryBlock() + { + $this->waitForEstimateShippingAndTaxForm(); + $this->waitForShippingMethodForm(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index fda29a2fe78fc..f632cdc3d7464 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -264,4 +264,15 @@ public function waitForShippingPriceBlock() { $this->waitForElementVisible($this->shippingPriceBlockSelector, Locator::SELECTOR_CSS); } + + /** + * Wait for "Grand Total" row to appear. + * + * @return void + */ + public function waitForGrandTotal() + { + $this->waitForUpdatedTotals(); + $this->waitForElementVisible($this->grandTotal); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php index a4da0c0ce604d..ac6889323ba00 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -21,6 +22,8 @@ */ class AssertCartItemsOptions extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Error message for verify options * @@ -44,6 +47,8 @@ class AssertCartItemsOptions extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php index 8806f25c3fddd..7ced144b10f46 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php @@ -6,6 +6,7 @@ namespace Magento\Checkout\Test\Constraint; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Constraint\AbstractConstraint; @@ -16,6 +17,8 @@ */ class AssertGrandTotalInShoppingCart extends AbstractConstraint { + use CartPageLoadTrait; + /** * Assert that grand total is equal to expected * @@ -28,6 +31,7 @@ public function processAssert(CheckoutCart $checkoutCart, Cart $cart, $requireRe { if ($requireReload) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); $checkoutCart->getTotalsBlock()->waitForUpdatedTotals(); } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php index 799fced1210dd..b913fd83cc074 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertPriceInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that price in the shopping cart equals to expected price from data set * @@ -29,6 +32,8 @@ class AssertPriceInShoppingCart extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php index f128905e74a0a..a343c2d8f466c 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertProductQtyInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that quantity in the shopping cart is equals to expected quantity from data set * @@ -29,6 +32,8 @@ class AssertProductQtyInShoppingCart extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php index cd6e29410d571..fe593ccc76c6f 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertSubtotalInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that subtotal total in the shopping cart is equals to expected total from data set * @@ -31,6 +34,7 @@ public function processAssert(CheckoutCart $checkoutCart, Cart $cart, $requireRe { if ($requireReload) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); } /** @var Items $sourceProducts */ diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php new file mode 100644 index 0000000000000..6b36e0ae65a3a --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Checkout\Test\Constraint\Utils; + +use Magento\Checkout\Test\Page\CheckoutCart; + +/** + * Check if cart page is fully loaded. + */ +trait CartPageLoadTrait +{ + /** + * @param CheckoutCart $checkoutCart + * @return void + */ + public function waitForCartPageLoaded(CheckoutCart $checkoutCart) + { + $checkoutCart->getCartBlock()->waitForLoader(); + if (!$checkoutCart->getCartBlock()->cartIsEmpty()) { + $checkoutCart->getShippingBlock()->waitForSummaryBlock(); + $checkoutCart->getTotalsBlock()->waitForGrandTotal(); + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php index 5935a68c43c21..af267cfa30ec1 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php @@ -115,6 +115,7 @@ public function test( } else { $miniShoppingCart->getCartItem($newProduct)->clickEditItem(); $this->catalogProductView->getViewBlock()->addToCart($newProduct); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); } // Prepare data for asserts: $cart['data']['items'] = ['products' => [$newProduct]]; diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php index 30e790f978c42..1a4cb787bf1c7 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php @@ -102,10 +102,12 @@ public function test( $productUrl = $_ENV['app_frontend_url'] . $product->getUrlKey() . '.html'; $browser->open($productUrl); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); if ($isGuest) { $this->objectManager->create(\Magento\Customer\Test\TestStep\LogoutCustomerOnFrontendStep::class)->run(); $browser->open($productUrl); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php index 2b0bd7178cdad..91b4f711700b5 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php @@ -144,6 +144,7 @@ public function test(Customer $customer, $product) )->run(); $this->browser->open($_ENV['app_frontend_url'] . $product->getUrlKey() . '.html'); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); //Steps $this->customerIndex->open(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php index 723ff963e2bfc..ea151c2f68750 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php @@ -46,7 +46,8 @@ protected function setUp() [ 'baseTmpPath' => $this->mediaDirectory->getRelativePath('tmp'), 'basePath' => __DIR__, - 'allowedExtensions' => ['jpg', 'jpeg', 'gif', 'png'] + 'allowedExtensions' => ['jpg', 'jpeg', 'gif', 'png'], + 'allowedMimeTypes' => ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'] ] ); } 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 f9075a58c39ef..2411d333a5a19 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php @@ -23,6 +23,11 @@ class UrlTest extends \PHPUnit\Framework\TestCase */ protected $urlPathGenerator; + /** + * @var \Magento\Framework\Registry + */ + private $registry; + protected function setUp() { $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -31,23 +36,22 @@ protected function setUp() $this->urlPathGenerator = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class ); + + /** @var \Magento\Framework\Registry $registry */ + $this->registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Registry::class + ); } public function testGetUrlInStore() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class - ); - $product = $repository->get('simple'); + $product = $this->getProduct(); $this->assertStringEndsWith('simple-product.html', $this->_model->getUrlInStore($product)); } public function testGetProductUrl() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class - ); - $product = $repository->get('simple'); + $product = $this->getProduct(); $this->assertStringEndsWith('simple-product.html', $this->_model->getProductUrl($product)); } @@ -80,10 +84,7 @@ public function testGetUrlPath() */ public function testGetUrl() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class - ); - $product = $repository->get('simple'); + $product = $this->getProduct(); $this->assertStringEndsWith('simple-product.html', $this->_model->getUrl($product)); $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -92,4 +93,83 @@ public function testGetUrl() $product->setId(100); $this->assertContains('catalog/product/view/id/100/', $this->_model->getUrl($product)); } + + /** + * @magentoAppArea frontend + * + * @magentoDataFixture Magento/Catalog/_files/config_use_category_in_url.php + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php + * + * @return void + */ + public function testGetUrlWithCategoryInUrl() + { + $product = $this->getProduct(); + $category = $this->getCategory($product); + + $this->assertStringEndsWith( + $category->getUrlKey() . '/' . $product->getUrlKey() . '.html', + $this->_model->getUrl($product) + ); + } + + /** + * @magentoAppArea frontend + * + * @return void + */ + public function testGetUrlWithoutCategoryInUrl() + { + $product = $this->getProduct(); + $category = $this->getCategory($product); + + $url = $this->_model->getUrl($product); + + $this->assertStringEndsWith($product->getUrlKey() . '.html', $url); + $this->assertNotContains($url, $category->getUrlKey()); + } + + /** + * @return \Magento\Catalog\Api\Data\ProductInterface + */ + private function getProduct(): \Magento\Catalog\Api\Data\ProductInterface + { + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class + ); + + return $productRepository->get('simple'); + } + + /** + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @return \Magento\Catalog\Api\Data\CategoryInterface + */ + private function getCategory( + \Magento\Catalog\Api\Data\ProductInterface $product + ): \Magento\Catalog\Api\Data\CategoryInterface { + /** @var \Magento\Catalog\Model\CategoryRepository $categoryRepository */ + $categoryRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\CategoryRepository::class + ); + + $categoryId = $product->getCategoryIds()[0]; + + $category = $categoryRepository->get($categoryId); + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + + return $category; + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php index a14f58874adcb..6ac7a8551fd99 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php @@ -127,6 +127,36 @@ public function testGetProductsWithTierPrice() $this->assertEquals(5, $tierPrices[2]->getValue()); } + /** + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoDbIsolation disabled + */ + public function testGetProductsWithSpecialPrice() + { + $product = $this->productRepository->get('simple'); + $originalFinalPrice = $product->getFinalPrice(); + + $specialPrice = 9; + $product->setSpecialPrice($specialPrice); + $product = $this->productRepository->save($product); + /** @var \Magento\Catalog\Model\Product $item */ + $item = $this->collection->addIdFilter($product->getId()) + ->addPriceData() + ->getFirstItem(); + $item->setPriceCalculation(false); + $this->assertEquals($specialPrice, $item->getFinalPrice()); + + $product->setSpecialPrice(null); + $product = $this->productRepository->save($product); + /** @var \Magento\Catalog\Model\Product $item */ + $item = $this->collection->clear() + ->addIdFilter($product->getId()) + ->addPriceData() + ->getFirstItem(); + $item->setPriceCalculation(false); + $this->assertEquals($originalFinalPrice, $item->getFinalPrice()); + } + /** * Test addAttributeToSort() with attribute 'is_saleable' works properly on frontend. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url.php new file mode 100644 index 0000000000000..b1c3aa755fc04 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Helper\Product; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var \Magento\Config\Model\Config $config */ +$config = $objectManager->get(\Magento\Config\Model\Config::class); +$config->setDataByPath(Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, 1); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url_rollback.php new file mode 100644 index 0000000000000..ff0e0784987dd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/config_use_category_in_url_rollback.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Helper\Product; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var \Magento\Config\Model\Config $config */ +$config = $objectManager->get(\Magento\Config\Model\Config::class); +$config->setDataByPath(Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, 0); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index b3e5e96c18cac..0d07b8c913e14 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -5,6 +5,7 @@ */ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); @@ -19,10 +20,15 @@ $tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); /** @var $tpExtensionAttributes */ $tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); $adminWebsite = $objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class)->get('admin'); $tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); $tierPrices[] = $tierPriceFactory->create( [ @@ -82,6 +88,7 @@ ->setTaxClassId(0) ->setTierPrices($tierPrices) ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 019fffc8c0898..f3cb01a9bbe2e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1070,6 +1070,7 @@ public function testProductsWithMultipleStores() * * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDataFixture Magento/Catalog/_files/category_with_two_stores.php + * @magentoDataFixture Magento/Catalog/_files/config_use_category_in_url.php * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ @@ -1573,15 +1574,10 @@ public function testExistingProductWithUrlKeys() */ public function testImportWithoutUrlKeys() { - /** - * Products `simple1` and `simple2` are created by fixture so already - * have a URL Key, whereas `new-simple` is a new product so the import - * will generate it's URL Key from the name provided in the CSV. - */ $products = [ - 'simple1' => 'url-key', - 'simple2' => 'url-key2', - 'new-simple' => 'new-simple', + 'simple1' => 'simple-1', + 'simple2' => 'simple-2', + 'simple3' => 'simple-3' ]; $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -1645,6 +1641,45 @@ public function testImportWithUrlKeysWithSpaces() } } + /** + * Make sure the absence of a url_key column in the csv file won't erase the url key of the existing products. + * To reach the goal we need to not send the name column, as the url key is generated from it. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithoutUrlKeysAndName() + { + $products = [ + 'simple1' => 'url-key', + 'simple2' => 'url-key2', + ]; + $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' => __DIR__ . '/_files/products_to_import_without_url_keys_and_name.csv', + 'directory' => $directory + ] + ); + + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + ) + ->setSource($source) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + foreach ($products as $productSku => $productUrlKey) { + $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); + } + } + /** * @magentoAppIsolation enabled */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv index 80898c5440df7..ff1b9f4b02afb 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys.csv @@ -2,4 +2,3 @@ sku,product_type,store_view_code,name,price,attribute_set_code,url_key simple1,simple,,"simple 1",25,Default,"" simple2,simple,,"simple 2",34,Default,"" simple3,simple,,"simple 3",58,Default,"" -new-simple,simple,,"new simple",25,Default,"" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv new file mode 100644 index 0000000000000..8ea6ab92a0295 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv @@ -0,0 +1,3 @@ +sku,price +simple1,25 +simple2,34 diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php index ba7df011eec9b..3d88ba391ee0a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php @@ -110,4 +110,16 @@ public static function publicChildStatic() final public function publicChildFinal() { } + + /** + * Test method + * + * @param bool $arg + * @return SourceClassWithNamespace + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function publicWithSelf($arg = false): self + { + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample index cf8611866a885..3e0389052ac07 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample @@ -54,6 +54,19 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN } } + /** + * {@inheritdoc} + */ + public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace + { + $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicWithSelf'); + if (!$pluginInfo) { + return parent::publicWithSelf($arg); + } else { + return $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo); + } + } + /** * {@inheritdoc} */ 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 345ac49c6a4f4..30112dcb51485 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 @@ -114,6 +114,14 @@ class Proxy extends \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespa return $this->_getSubject()->publicChildWithoutParameters(); } + /** + * {@inheritdoc} + */ + public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace + { + return $this->_getSubject()->publicWithSelf($arg); + } + /** * {@inheritdoc} */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/MessageTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/MessageTest.php deleted file mode 100644 index d2b220b38f695..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Mail/MessageTest.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Framework\Mail; - -class MessageTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Message - */ - private $message; - - /** - * @inheritdoc - */ - protected function setUp() - { - $this->message = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(Message::class); - } - - public function testGetHeaderEncodingDefaultValue() - { - $this->assertEquals(\Zend_Mime::ENCODING_BASE64, $this->message->getHeaderEncoding()); - } - - public function testGetCharsetDefaultValue() - { - $this->assertEquals('utf-8', $this->message->getCharset()); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/UnSubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/UnSubscriberTest.php new file mode 100644 index 0000000000000..9cb526624bb11 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/UnSubscriberTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Newsletter\Controller; + +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Framework\App\Config\Value; + +/** + * Test Unsubscriber controller. + * + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + * @magentoAppArea frontend + */ +class UnSubscriberTest extends AbstractController +{ + /** + * @var Subscriber + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Newsletter\Model\Subscriber::class + ); + parent::setUp(); + } + + /** + * @return void + */ + public function testSuccessUnsubscribeSubscribedUser() + { + $subscriber = $this->model->loadByCustomerId(1); + $this->getRequest() + ->setParam('id', $subscriber->getId()) + ->setParam('code', 'zxayquyajua23iq29gxwu2eax2qb6gvy'); + + $this->dispatch('newsletter/subscriber/unsubscribe'); + + $this->assertSessionMessages($this->equalTo(['You unsubscribed.'])); + $this->assertRedirect($this->equalTo($this->getBaseUrl() . 'index.php/')); + } + + /** + * @return void + */ + public function testFailureUnsubscribeSubscribedUser() + { + $subscriber = $this->model->loadByCustomerId(1); + $this->getRequest() + ->setParam('id', $subscriber->getId()) + ->setParam('code', 'randomcode'); + + $this->dispatch('newsletter/subscriber/unsubscribe'); + + $this->assertSessionMessages($this->equalTo(['This is an invalid subscription confirmation code.'])); + $this->assertRedirect($this->equalTo($this->getBaseUrl() . 'index.php/')); + } + + /** + * @return string + */ + private function getBaseUrl() + { + $configValue = $this->_objectManager->create(Value::class); + $configValue->load('web/unsecure/base_url', 'path'); + + return $configValue->getValue() ?: 'http://localhost/'; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php index 6e356694b2f03..c6ba498319ff8 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php @@ -33,7 +33,7 @@ public function testEmailConfirmation() $this->assertContains( '/newsletter/subscriber/confirm/id/' . $this->_model->getSubscriberId() . '/code/ysayquyajua23iq29gxwu2eax2qb6gvy', - $transportBuilder->getSentMessage()->getBodyHtml()->getRawContent() + $transportBuilder->getSentMessage()->getRawMessage() ); $this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->_model->getSubscriberStatus()); } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/_files/subscribers.php b/dev/tests/integration/testsuite/Magento/Newsletter/_files/subscribers.php index 84e72979cc4c5..b55c471c11235 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/_files/subscribers.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/_files/subscribers.php @@ -22,6 +22,7 @@ $subscriber->setStoreId($currentStore) ->setCustomerId(1) ->setSubscriberEmail('customer@example.com') + ->setSubscriberConfirmCode('zxayquyajua23iq29gxwu2eax2qb6gvy') ->setSubscriberStatus(\Magento\Newsletter\Model\Subscriber::STATUS_SUBSCRIBED) ->save(); $firstSubscriberId = $subscriber->getId(); diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php new file mode 100644 index 0000000000000..52551e6dc96d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\ResourceConnection; +use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$website = $websiteRepository->get('test'); + +/** @var ResourceConnection $resource */ +$resource = $objectManager->get(ResourceConnection::class); +$connection = $resource->getConnection(); +$resourceModel = $objectManager->create(Tablerate::class); +$entityTable = $resourceModel->getTable('shipping_tablerate'); +$data = + [ + 'website_id' => $website->getId(), + 'dest_country_id' => 'US', + 'dest_region_id' => 0, + 'dest_zip' => '*', + 'condition_name' => 'package_qty', + 'condition_value' => 1, + 'price' => 20, + 'cost' => 20 + ]; +$connection->query( + "INSERT INTO {$entityTable} (`website_id`, `dest_country_id`, `dest_region_id`, `dest_zip`, `condition_name`," + . "`condition_value`, `price`, `cost`) VALUES (:website_id, :dest_country_id, :dest_region_id, :dest_zip," + . " :condition_name, :condition_value, :price, :cost);", + $data +); diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website_rollback.php new file mode 100644 index 0000000000000..9606b0eb605fd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website_rollback.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class); +$connection = $resource->getConnection(); +$resourceModel = $objectManager->create(\Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate::class); +$entityTable = $resourceModel->getTable('shipping_tablerate'); +$connection->query("DELETE FROM {$entityTable};"); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index 412ecd80847c1..eb80da6d21b19 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -293,7 +293,9 @@ public function testReturnFromPaypal() /** * The case when handling address data from Paypal button. * System's address fields are replacing from export Paypal data. + * Billing and Shipping address are the same. * + * @return void * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled @@ -307,39 +309,96 @@ public function testReturnFromPaypalButton() $this->checkoutModel->returnFromPaypal('token'); $shippingAddress = $quote->getShippingAddress(); - - $prefix = ''; - $this->assertEquals([$prefix . $this->getExportedData()['street']], $shippingAddress->getStreet()); - $this->assertEquals($prefix . $this->getExportedData()['firstname'], $shippingAddress->getFirstname()); - $this->assertEquals($prefix . $this->getExportedData()['city'], $shippingAddress->getCity()); - $this->assertEquals($prefix . $this->getExportedData()['telephone'], $shippingAddress->getTelephone()); - $this->assertEquals($prefix . $this->getExportedData()['email'], $shippingAddress->getEmail()); + $billingAddress = $quote->getBillingAddress(); + $exportedShippingData = $this->getExportedData()['shipping']; + + $this->assertEquals([$exportedShippingData['street']], $shippingAddress->getStreet()); + $this->assertEquals($exportedShippingData['firstname'], $shippingAddress->getFirstname()); + $this->assertEquals($exportedShippingData['city'], $shippingAddress->getCity()); + $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); + $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); + + $this->assertEquals([$exportedShippingData['street']], $billingAddress->getStreet()); + $this->assertEquals($exportedShippingData['firstname'], $billingAddress->getFirstname()); + $this->assertEquals($exportedShippingData['city'], $billingAddress->getCity()); + $this->assertEquals($exportedShippingData['telephone'], $billingAddress->getTelephone()); + $this->assertEquals($exportedShippingData['email'], $billingAddress->getEmail()); } /** * The case when handling address data from the checkout. * System's address fields are not replacing from export PayPal data. + * Billing and Shipping address are the same * + * @return void * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ public function testReturnFromPaypalIfCheckout() { + $prefix = 'exported'; $quote = $this->getFixtureQuote(); - $this->prepareCheckoutModel($quote); + $this->prepareCheckoutModel($quote, $prefix); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 0); + $originalShippingAddress = $quote->getShippingAddress(); + $originalBillingAddress = $quote->getBillingAddress(); + $this->checkoutModel->returnFromPaypal('token'); $shippingAddress = $quote->getShippingAddress(); + $billingAddress = $quote->getBillingAddress(); + + $this->assertEquals($originalShippingAddress->getStreet(), $shippingAddress->getStreet()); + $this->assertEquals($originalShippingAddress->getFirstname(), $shippingAddress->getFirstname()); + $this->assertEquals($originalShippingAddress->getCity(), $shippingAddress->getCity()); + $this->assertEquals($originalShippingAddress->getTelephone(), $shippingAddress->getTelephone()); + $this->assertEquals($originalBillingAddress->getStreet(), $billingAddress->getStreet()); + $this->assertEquals($originalBillingAddress->getFirstname(), $billingAddress->getFirstname()); + $this->assertEquals($originalBillingAddress->getCity(), $billingAddress->getCity()); + $this->assertEquals($originalBillingAddress->getTelephone(), $billingAddress->getTelephone()); + } + + /** + * The case when handling address data from the checkout. + * System's address fields are replacing billing address from export PayPal data. + * Billing and Shipping address are different + * + * @return void + * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testReturnFromPaypalIfCheckoutWithReturnBillingAddress() + { $prefix = 'exported'; + $quote = $this->getFixtureQuote(); + $this->paypalConfig->expects($this->exactly(2)) + ->method('getValue') + ->with('requireBillingAddress') + ->willReturn(1); + $this->prepareCheckoutModel($quote, $prefix); + $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 0); + + $originalShippingAddress = $quote->getShippingAddress(); + + $this->checkoutModel->returnFromPaypal('token'); - $this->assertNotEquals([$prefix . $this->getExportedData()['street']], $shippingAddress->getStreet()); - $this->assertNotEquals($prefix . $this->getExportedData()['firstname'], $shippingAddress->getFirstname()); - $this->assertNotEquals($prefix . $this->getExportedData()['city'], $shippingAddress->getCity()); - $this->assertNotEquals($prefix . $this->getExportedData()['telephone'], $shippingAddress->getTelephone()); + $shippingAddress = $quote->getShippingAddress(); + $billingAddress = $quote->getBillingAddress(); + $exportedBillingData = $this->getExportedData()['billing']; + + $this->assertEquals($originalShippingAddress->getStreet(), $shippingAddress->getStreet()); + $this->assertEquals($originalShippingAddress->getFirstname(), $shippingAddress->getFirstname()); + $this->assertEquals($originalShippingAddress->getCity(), $shippingAddress->getCity()); + $this->assertEquals($originalShippingAddress->getTelephone(), $shippingAddress->getTelephone()); + + $this->assertEquals([$prefix . $exportedBillingData['street']], $billingAddress->getStreet()); + $this->assertEquals($prefix . $exportedBillingData['firstname'], $billingAddress->getFirstname()); + $this->assertEquals($prefix . $exportedBillingData['city'], $billingAddress->getCity()); + $this->assertEquals($prefix . $exportedBillingData['telephone'], $billingAddress->getTelephone()); } /** @@ -347,6 +406,7 @@ public function testReturnFromPaypalIfCheckout() * Customer add virtual product to quote and place order using PayPal Express method. * After return from PayPal quote billing address have to be updated by PayPal Express address. * + * @return void * @magentoDataFixture Magento/Paypal/_files/virtual_quote_with_empty_billing_address.php * @magentoConfigFixture current_store payment/paypal_express/active 1 * @magentoDbIsolation enabled @@ -361,7 +421,7 @@ public function testReturnFromPaypalForCustomerWithEmptyAddresses() $billingAddress = $quote->getBillingAddress(); - $this->performQuoteAddressAssertions($billingAddress, $this->getExportedData()); + $this->performQuoteAddressAssertions($billingAddress, $this->getExportedData()['billing']); } /** @@ -430,8 +490,10 @@ private function performQuoteAddressAssertions(Address $address, array $expected * Initialize a checkout model mock. * * @param Quote $quote + * @param string $prefix + * @return void */ - private function prepareCheckoutModel(Quote $quote) + private function prepareCheckoutModel(Quote $quote, $prefix = '') { $this->checkoutModel = $this->objectManager->create( Checkout::class, @@ -442,11 +504,11 @@ private function prepareCheckoutModel(Quote $quote) ] ); - $exportedBillingAddress = $this->getExportedAddressFixture($this->getExportedData()); + $exportedBillingAddress = $this->getExportedAddressFixture($this->getExportedData()['billing'], $prefix); $this->api->method('getExportedBillingAddress') ->will($this->returnValue($exportedBillingAddress)); - $exportedShippingAddress = $this->getExportedAddressFixture($this->getExportedData()); + $exportedShippingAddress = $this->getExportedAddressFixture($this->getExportedData()['shipping'], $prefix); $this->api->method('getExportedShippingAddress') ->will($this->returnValue($exportedShippingAddress)); @@ -459,19 +521,33 @@ private function prepareCheckoutModel(Quote $quote) * * @return array */ - private function getExportedData() + private function getExportedData(): array { return [ - 'email' => 'customer@example.com', - 'firstname' => 'John', - 'lastname' => 'Doe', - 'country' => 'US', - 'region' => 'Colorado', - 'region_id' => '13', - 'city' => 'Denver', - 'street' => '66 Pearl St', - 'postcode' => '80203', - 'telephone' => '555-555-555', + 'shipping' => [ + 'email' => 'customer@example.com', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'country' => 'US', + 'region' => 'Colorado', + 'region_id' => '13', + 'city' => 'Denver', + 'street' => '66 Pearl St', + 'postcode' => '80203', + 'telephone' => '555-555-555', + ], + 'billing' => [ + 'email' => 'customer@example.com', + 'firstname' => 'Jane', + 'lastname' => 'Doe', + 'country' => 'US', + 'region' => 'Texas', + 'region_id' => '13', + 'city' => 'Austin', + 'street' => '1100 Congress Ave', + 'postcode' => '78701', + 'telephone' => '555-555-555', + ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php index b545bb8a9f742..aed9214899237 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php @@ -82,7 +82,7 @@ public function testSend($isCustomerIdUsed) ); $this->assertContains( 'John Smith,', - $transportBuilder->getSentMessage()->getBodyHtml()->getRawContent() + $transportBuilder->getSentMessage()->getRawMessage() ); } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php index 50213710233d2..0fc98d8d8380b 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php @@ -56,7 +56,7 @@ public function testProcess() ); $this->assertContains( 'John Smith,', - $transportBuilder->getSentMessage()->getBodyHtml()->getRawContent() + $transportBuilder->getSentMessage()->getRawMessage() ); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php index e071dde26a263..7110f39ee532c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php @@ -6,12 +6,26 @@ namespace Magento\Sales\Controller\Adminhtml\Order; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Backend\Model\Session\Quote; +use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\ScopeInterface; /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class CreateTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -57,6 +71,66 @@ public function testLoadBlockActionData() } /** + * Tests that shipping method 'Table rates' shows rates according to selected website. + * + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Quote/Fixtures/quote_sec_website.php + * @magentoDataFixture Magento/OfflineShipping/_files/tablerates_second_website.php + * @magentoDbIsolation disabled + */ + public function testLoadBlockShippingMethod() + { + $store = $this->getStore('fixture_second_store'); + + /** @var MutableScopeConfigInterface $mutableScopeConfig */ + $mutableScopeConfig = $this->_objectManager->get(MutableScopeConfigInterface::class); + $mutableScopeConfig->setValue( + 'carriers/tablerate/active', + 1, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $mutableScopeConfig->setValue( + 'carriers/tablerate/condition_name', + 'package_qty', + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $website = $this->getWebsite('test'); + $customer = $this->getCustomer('customer.web@example.com', (int)$website->getId()); + $quote = $this->getQuoteById('0000032134'); + $session = $this->_objectManager->get(SessionQuote::class); + $session->setQuoteId($quote->getId()); + + $data = [ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'street' => ['Soborna 23'], + 'city' => 'Testcity', + 'country_id' => 'US', + 'region' => 'Alabama', + 'region_id' => 1 + ]; + $this->getRequest()->setPostValue( + [ + 'order' => ['billing_address' => $data], + 'reset_shipping' => 1, + 'collect_shipping_rates' => 1, + 'customer_id' => $customer->getId(), + 'store_id' => $store->getId(), + 'json' => true + ] + ); + $this->dispatch('backend/sales/order_create/loadBlock/block/shipping_method'); + $body = $this->getResponse()->getBody(); + $expectedTableRatePrice = '<span class=\"price\">$20.00<\/span>'; + + $this->assertContains($expectedTableRatePrice, $body, ''); + } + + /** + * Tests LoadBlock actions. + * * @dataProvider loadBlockActionsDataProvider */ public function testLoadBlockActions($block, $expected) @@ -80,6 +154,8 @@ public function loadBlockActionsDataProvider() } /** + * Tests action items. + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ public function testLoadBlockActionItems() @@ -153,6 +229,8 @@ public function testIndexAction() } /** + * Tests ACL. + * * @param string $actionName * @param boolean $reordered * @param string $expectedResult @@ -162,7 +240,7 @@ public function testIndexAction() */ public function testGetAclResource($actionName, $reordered, $expectedResult) { - $this->_objectManager->get(Quote::class)->setReordered($reordered); + $this->_objectManager->get(SessionQuote::class)->setReordered($reordered); $orderController = $this->_objectManager->get( \Magento\Sales\Controller\Adminhtml\Order\Stub\OrderCreateStub::class ); @@ -251,7 +329,7 @@ public function testSyncBetweenQuoteAddresses() $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $quote = $quoteRepository->getActiveForCustomer($customer->getId()); - $session = $this->_objectManager->get(Quote::class); + $session = $this->_objectManager->get(SessionQuote::class); $session->setQuoteId($quote->getId()); $data = [ @@ -286,4 +364,69 @@ public function testSyncBetweenQuoteAddresses() self::assertEquals($data['city'], $shippingAddress->getCity()); self::assertEquals($data['street'], $shippingAddress->getStreet()); } + + /** + * Gets quote entity by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuoteById(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $repository */ + $repository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Gets website entity. + * + * @param string $code + * @return WebsiteInterface + * @throws NoSuchEntityException + */ + private function getWebsite(string $code): WebsiteInterface + { + /** @var WebsiteRepositoryInterface $repository */ + $repository = $this->_objectManager->get(WebsiteRepositoryInterface::class); + return $repository->get($code); + } + + /** + * Gets customer entity. + * + * @param string $email + * @param int $websiteId + * @return CustomerInterface + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getCustomer(string $email, int $websiteId): CustomerInterface + { + /** @var CustomerRepositoryInterface $repository */ + $repository = $this->_objectManager->get(CustomerRepositoryInterface::class); + return $repository->get($email, $websiteId); + } + + /** + * Gets store by code. + * + * @param string $code + * @return StoreInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getStore(string $code): StoreInterface + { + /** @var StoreRepositoryInterface $repository */ + $repository = $this->_objectManager->get(StoreRepositoryInterface::class); + return $repository->get($code); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php new file mode 100644 index 0000000000000..8eda87ae43c29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\ResourceModel\Order\Grid; + +use Magento\TestFramework\Helper\Bootstrap; + +class CollectionTest extends \PHPUnit\Framework\TestCase +{ + /** + * Tests collection properties. + * + * @throws \ReflectionException + * @return void + */ + public function testCollectionCreate() + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var Collection $gridCollection */ + $gridCollection = $objectManager->get(Collection::class); + $tableDescription = $gridCollection->getConnection() + ->describeTable($gridCollection->getMainTable()); + + $mapper = new \ReflectionMethod( + Collection::class, + '_getMapper' + ); + $mapper->setAccessible(true); + $map = $mapper->invoke($gridCollection); + + self::assertInternalType('array', $map); + self::assertArrayHasKey('fields', $map); + self::assertInternalType('array', $map['fields']); + self::assertCount(count($tableDescription), $map['fields']); + + foreach ($map['fields'] as $mappedName) { + self::assertContains('main_table.', $mappedName); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php index 6a00031434b3d..dd58f4eb7a984 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php @@ -10,6 +10,7 @@ use Magento\Store\Model\Store; use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndex; use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Store\Model\Group; $objectManager = Bootstrap::getObjectManager(); //Creating second website with a store. @@ -21,12 +22,23 @@ $website->setData([ 'code' => 'test', 'name' => 'Test Website', - 'default_group_id' => '1', 'is_default' => '0', ]); $website->save(); } +/** + * @var Group $storeGroup + */ +$storeGroup = $objectManager->create(Group::class); +$storeGroup->setCode('some_group') + ->setName('custom store group') + ->setWebsite($website); +$storeGroup->save($storeGroup); + +$website->setDefaultGroupId($storeGroup->getId()); +$website->save($website); + $websiteId = $website->getId(); $store = $objectManager->create(Store::class); $store->load('fixture_second_store', 'code'); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php index 4999e379e1d0a..92eae7a3fe3d7 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php @@ -170,9 +170,7 @@ public function testSendAction() \Magento\TestFramework\Mail\Template\TransportBuilderMock::class ); - $actualResult = \Zend_Mime_Decode::decodeQuotedPrintable( - $transportBuilder->getSentMessage()->getBodyHtml()->getContent() - ); + $actualResult = quoted_printable_decode($transportBuilder->getSentMessage()->getRawMessage()); $this->assertStringMatchesFormat( '%A' . $this->_customerViewHelper->getCustomerName($this->_customerSession->getCustomerDataObject()) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js index 52739eec2782b..df6996afeb965 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js @@ -19,7 +19,12 @@ define([ billingAddress: ko.observable(), shippingAddress: ko.observable(), paymentMethod: ko.observable(), - totals: ko.observable({}) + totals: ko.observable({}), + + /** Stub */ + isVirtual: function () { + return false; + } }, 'Magento_Braintree/js/view/payment/validator-handler': jasmine.createSpyObj( 'validator-handler', diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js index bcfaecd277754..a2373cfb99091 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js @@ -24,7 +24,12 @@ define([ paymentMethod: ko.observable(), totals: ko.observable({ 'base_grand_total': 0 - }) + }), + + /** Stub */ + isVirtual: function () { + return false; + } }, 'Magento_Braintree/js/view/payment/adapter': { config: {}, diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js index 216ad28cf496a..0748fb64aca56 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js @@ -143,5 +143,12 @@ define([ }); expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals).toHaveBeenCalled(); }); + + it('test subscribe when cart data was changed', function () { + mocks['Magento_Customer/js/customer-data'].get('cart')({ + dataId: 2 + }); + expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals).toHaveBeenCalled(); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js index 47e3507ea1321..12e12eb492c89 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js @@ -28,8 +28,12 @@ define([ billingAddress: ko.observable(), shippingAddress: ko.observable(), paymentMethod: ko.observable(), - totals: ko.observable({}) + totals: ko.observable({}), + /** Stub */ + isVirtual: function () { + return false; + } }, 'Magento_Checkout/js/action/set-payment-information': setPaymentMock, 'Magento_Checkout/js/model/payment/additional-validators': { diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/sticky/sticky.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/sticky/sticky.test.js index 40bfad0519ec6..9a6c28757e89f 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/sticky/sticky.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/sticky/sticky.test.js @@ -46,8 +46,10 @@ define([ expect(stickyObj.initListeners).toHaveBeenCalled(); }); it('has initOnScroll method', function () { + spyOn(document, 'addEventListener'); stickyObj.initOnScroll(); expect(stickyObj.lastHorizontalScrollPos).toBeDefined(); + expect(document.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); }); it('has initOnListingScroll method', function () { spyOn(stickyObj, 'initOnListingScroll'); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php index 81088522bccb2..a4baacbe4d169 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php @@ -96,35 +96,6 @@ function ($file) { ); } - public function testAppCodeUsage() - { - $files = Files::init(); - $componentRegistrar = new ComponentRegistrar(); - $libPaths = $componentRegistrar->getPaths(ComponentRegistrar::LIBRARY); - $invoker = new AggregateInvoker($this); - $invoker( - function ($file) use ($libPaths) { - $content = file_get_contents($file); - foreach ($libPaths as $libPath) { - if (strpos($file, $libPath) === 0) { - $this->assertSame( - 0, - preg_match('~(?<![a-z\\d_:]|->|function\\s)__\\s*\\(~iS', $content), - 'Function __() is defined outside of the library and must not be used there. ' . - 'Replacement suggestion: new \\Magento\\Framework\\Phrase()' - ); - } - } - }, - $files->getPhpFiles( - Files::INCLUDE_PUB_CODE | - Files::INCLUDE_LIBS | - Files::AS_DATA_SET | - Files::INCLUDE_NON_CLASSES - ) - ); - } - /** * @inheritdoc */ diff --git a/dev/travis/before_script.sh b/dev/travis/before_script.sh index 81e9ee146b495..b49d9acf4a701 100755 --- a/dev/travis/before_script.sh +++ b/dev/travis/before_script.sh @@ -72,7 +72,7 @@ case $TEST_SUITE in --base-path="$TRAVIS_BUILD_DIR" \ --repo='https://github.com/magento/magento2.git' \ --branch="$TRAVIS_BRANCH" - cat "$changed_files_ce" | sed 's/^/ + including /' + sed 's/^/ + including /' "$changed_files_ce" cd ../../.. ;; diff --git a/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php b/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php index 321997afdba95..681af35944695 100644 --- a/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php +++ b/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\App\Config; +use Magento\Framework\App\ScopeInterface; use Magento\Framework\App\ScopeResolverPool; /** @@ -34,7 +35,7 @@ public function __construct(ScopeResolverPool $scopeResolverPool) * Resolve scope code * * @param string $scopeType - * @param string $scopeCode + * @param string|null $scopeCode * @return string */ public function resolve($scopeType, $scopeCode) @@ -42,20 +43,24 @@ public function resolve($scopeType, $scopeCode) if (isset($this->resolvedScopeCodes[$scopeType][$scopeCode])) { return $this->resolvedScopeCodes[$scopeType][$scopeCode]; } - if (($scopeCode === null || is_numeric($scopeCode)) - && $scopeType !== ScopeConfigInterface::SCOPE_TYPE_DEFAULT - ) { + + if ($scopeType !== ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { $scopeResolver = $this->scopeResolverPool->get($scopeType); $resolverScopeCode = $scopeResolver->getScope($scopeCode); } else { $resolverScopeCode = $scopeCode; } - if ($resolverScopeCode instanceof \Magento\Framework\App\ScopeInterface) { + if ($resolverScopeCode instanceof ScopeInterface) { $resolverScopeCode = $resolverScopeCode->getCode(); } + if ($scopeCode === null) { + $scopeCode = $resolverScopeCode; + } + $this->resolvedScopeCodes[$scopeType][$scopeCode] = $resolverScopeCode; + return $resolverScopeCode; } diff --git a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php index 608907abcc0e1..05f5f01efb29e 100644 --- a/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Code/Generator/Interceptor.php @@ -113,7 +113,7 @@ protected function _getMethodInfo(\ReflectionMethod $method) "} else {\n" . " return \$this->___callPlugins('{$method->getName()}', func_get_args(), \$pluginInfo);\n" . "}", - 'returnType' => $method->getReturnType(), + 'returnType' => $this->getReturnTypeValue($method->getReturnType()), 'docblock' => ['shortDescription' => '{@inheritdoc}'], ]; @@ -183,4 +183,23 @@ protected function _validateData() } return $result; } + + /** + * Returns return type + * + * @param mixed $returnType + * @return null|string + */ + private function getReturnTypeValue($returnType) + { + $returnTypeValue = null; + + if ($returnType) { + $returnTypeValue = ((string)$returnType === 'self') + ? $this->getSourceClassName() + : (string)$returnType; + } + + return $returnTypeValue; + } } diff --git a/lib/internal/Magento/Framework/Mail/MailMessageInterface.php b/lib/internal/Magento/Framework/Mail/MailMessageInterface.php new file mode 100644 index 0000000000000..4f16ad4392c0c --- /dev/null +++ b/lib/internal/Magento/Framework/Mail/MailMessageInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Mail; + +/** + * Mail Message interface + */ +interface MailMessageInterface extends MessageInterface +{ + /** + * Set mail message body in HTML format. + * + * @param string $html + * @return $this + */ + public function setBodyHtml($html); + + /** + * Set mail message body in text format. + * + * @param string $text + * @return $this + */ + public function setBodyText($text); + + /** + * Get message source code. + * + * @return string + */ + public function getRawMessage(); +} diff --git a/lib/internal/Magento/Framework/Mail/Message.php b/lib/internal/Magento/Framework/Mail/Message.php index f9511a260da1a..c8f0e75c52800 100644 --- a/lib/internal/Magento/Framework/Mail/Message.php +++ b/lib/internal/Magento/Framework/Mail/Message.php @@ -1,60 +1,176 @@ <?php /** - * Mail Message - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Mail; -class Message extends \Zend_Mail implements MessageInterface +use Zend\Mime\Mime; +use Zend\Mime\Part; + +class Message implements MailMessageInterface { /** + * @var \Zend\Mail\Message + */ + private $zendMessage; + + /** + * Message type + * + * @var string + */ + private $messageType = self::TYPE_TEXT; + + /** + * Initialize dependencies. + * * @param string $charset */ public function __construct($charset = 'utf-8') { - parent::__construct($charset); - $this->setHeaderEncoding(\Zend_Mime::ENCODING_BASE64); + $this->zendMessage = new \Zend\Mail\Message(); + $this->zendMessage->setEncoding($charset); } /** - * Message type + * {@inheritdoc} * - * @var string + * @deprecated + * @see \Magento\Framework\Mail\Message::setBodyText + * @see \Magento\Framework\Mail\Message::setBodyHtml */ - protected $messageType = self::TYPE_TEXT; + public function setMessageType($type) + { + $this->messageType = $type; + return $this; + } /** - * Set message body + * {@inheritdoc} * - * @param string $body - * @return $this + * @deprecated + * @see \Magento\Framework\Mail\Message::setBodyText + * @see \Magento\Framework\Mail\Message::setBodyHtml */ public function setBody($body) { - return $this->messageType == self::TYPE_TEXT ? $this->setBodyText($body) : $this->setBodyHtml($body); + if (is_string($body) && $this->messageType === MailMessageInterface::TYPE_HTML) { + $body = $this->createHtmlMimeFromString($body); + } + $this->zendMessage->setBody($body); + return $this; } /** - * Set message body - * - * @return string + * @inheritdoc + */ + public function setSubject($subject) + { + $this->zendMessage->setSubject($subject); + return $this; + } + + /** + * @inheritdoc + */ + public function getSubject() + { + return $this->zendMessage->getSubject(); + } + + /** + * @inheritdoc */ public function getBody() { - return $this->messageType == self::TYPE_TEXT ? $this->getBodyText() : $this->getBodyHtml(); + return $this->zendMessage->getBody(); } /** - * Set message type - * - * @param string $type - * @return $this + * @inheritdoc */ - public function setMessageType($type) + public function setFrom($fromAddress) { - $this->messageType = $type; + $this->zendMessage->setFrom($fromAddress); + return $this; + } + + /** + * @inheritdoc + */ + public function addTo($toAddress) + { + $this->zendMessage->addTo($toAddress); return $this; } + + /** + * @inheritdoc + */ + public function addCc($ccAddress) + { + $this->zendMessage->addCc($ccAddress); + return $this; + } + + /** + * @inheritdoc + */ + public function addBcc($bccAddress) + { + $this->zendMessage->addBcc($bccAddress); + return $this; + } + + /** + * @inheritdoc + */ + public function setReplyTo($replyToAddress) + { + $this->zendMessage->setReplyTo($replyToAddress); + return $this; + } + + /** + * @inheritdoc + */ + public function getRawMessage() + { + return $this->zendMessage->toString(); + } + + /** + * @inheritdoc + */ + public function setBodyHtml($html) + { + $this->setMessageType(self::TYPE_HTML); + return $this->setBody($html); + } + + /** + * @inheritdoc + */ + public function setBodyText($text) + { + $this->setMessageType(self::TYPE_TEXT); + return $this->setBody($text); + } + + /** + * Create HTML mime message from the string. + * + * @param string $htmlBody + * @return \Zend\Mime\Message + */ + private function createHtmlMimeFromString($htmlBody) + { + $htmlPart = new Part($htmlBody); + $htmlPart->setCharset($this->zendMessage->getEncoding()); + $htmlPart->setType(Mime::TYPE_HTML); + $mimeMessage = new \Zend\Mime\Message(); + $mimeMessage->addPart($htmlPart); + return $mimeMessage; + } } diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php index 95f17fed1123c..785c93824a57d 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php @@ -47,7 +47,6 @@ public function __construct( public function setFromByStore($from, $store) { $result = $this->senderResolver->resolve($from, $store); - $this->message->clearFrom(); $this->message->setFrom($result['email'], $result['name']); return $this; diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/MessageTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/MessageTest.php index ec484ede5f29c..0c33e25e0d9d0 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/MessageTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/MessageTest.php @@ -8,7 +8,7 @@ class MessageTest extends \PHPUnit\Framework\TestCase { /** - * @var \PHPUnit\Framework_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Mail\Message */ protected $_messageMock; @@ -16,78 +16,33 @@ protected function setUp() { $this->_messageMock = $this->createPartialMock( \Magento\Framework\Mail\Message::class, - ['getBodyText', 'getBodyHtml', 'setBodyText', 'setBodyHtml'] + ['setBody', 'setMessageType'] ); } - /** - * @param string $messageType - * @param string $method - * - * @covers \Magento\Framework\Mail\Message::setBody - * @covers \Magento\Framework\Mail\Message::setMessageType - * @dataProvider setBodyDataProvider - */ - public function testSetBody($messageType, $method) + public function testSetBodyHtml() { - $this->_messageMock->setMessageType($messageType); + $this->_messageMock->expects($this->once()) + ->method('setMessageType') + ->with('text/html'); $this->_messageMock->expects($this->once()) - ->method($method) + ->method('setBody') ->with('body'); - $this->_messageMock->setBody('body'); + $this->_messageMock->setBodyHtml('body'); } - /** - * @return array - */ - public function setBodyDataProvider() + public function testSetBodyText() { - return [ - [ - 'messageType' => 'text/plain', - 'method' => 'setBodyText', - ], - [ - 'messageType' => 'text/html', - 'method' => 'setBodyHtml' - ] - ]; - } - - /** - * @param string $messageType - * @param string $method - * - * @covers \Magento\Framework\Mail\Message::getBody - * @covers \Magento\Framework\Mail\Message::setMessageType - * @dataProvider getBodyDataProvider - */ - public function testGetBody($messageType, $method) - { - $this->_messageMock->setMessageType($messageType); - $this->_messageMock->expects($this->once()) - ->method($method); + ->method('setMessageType') + ->with('text/plain'); - $this->_messageMock->getBody('body'); - } + $this->_messageMock->expects($this->once()) + ->method('setBody') + ->with('body'); - /** - * @return array - */ - public function getBodyDataProvider() - { - return [ - [ - 'messageType' => 'text/plain', - 'method' => 'getBodyText', - ], - [ - 'messageType' => 'text/html', - 'method' => 'getBodyHtml' - ] - ]; + $this->_messageMock->setBodyText('body'); } } diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php index 58c9b045eed8c..80df2887a3a93 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php @@ -58,9 +58,6 @@ public function testSetFromByStore() ->method('setFrom') ->with('from@example.com', 'name') ->willReturnSelf(); - $this->messageMock->expects($this->once()) - ->method('clearFrom') - ->willReturnSelf(); $this->model->setFromByStore($sender, $store); } diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/TransportTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/TransportTest.php index be735e3a6fe2e..3cbe6fc923cdd 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/TransportTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/TransportTest.php @@ -7,48 +7,17 @@ class TransportTest extends \PHPUnit\Framework\TestCase { - /** - * @var \PHPUnit\Framework_MockObject - */ - protected $messageMock; - - /** - * @var \Magento\Framework\Mail\Transport - */ - protected $transport; - - protected function setUp() - { - $this->messageMock = $this->createMock(\Magento\Framework\Mail\Message::class); - $this->transport = new \Magento\Framework\Mail\Transport($this->messageMock); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage The message should be an instance of \Zend_Mail - */ - public function testTransportWithIncorrectMessageObject() - { - $this->messageMock = $this->createMock(\Magento\Framework\Mail\MessageInterface::class); - $this->transport = new \Magento\Framework\Mail\Transport($this->messageMock); - } - /** * @covers \Magento\Framework\Mail\Transport::sendMessage * @expectedException \Magento\Framework\Exception\MailException - * @expectedExceptionMessage No body specified + * @expectedExceptionMessage Invalid email; contains no at least one of "To", "Cc", and "Bcc" header */ public function testSendMessageBrokenMessage() { - $this->messageMock->expects($this->any()) - ->method('getParts') - ->will($this->returnValue(['a', 'b'])); - - $this->transport->sendMessage(); - } + $transport = new \Magento\Framework\Mail\Transport( + new \Magento\Framework\Mail\Message() + ); - public function testGetMessage() - { - $this->assertSame($this->messageMock, $this->transport->getMessage()); + $transport->sendMessage(); } } diff --git a/lib/internal/Magento/Framework/Mail/Transport.php b/lib/internal/Magento/Framework/Mail/Transport.php index dc3b48399c865..fd931c5cccecb 100644 --- a/lib/internal/Magento/Framework/Mail/Transport.php +++ b/lib/internal/Magento/Framework/Mail/Transport.php @@ -1,44 +1,48 @@ <?php /** - * Mail Transport * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Mail; -class Transport extends \Zend_Mail_Transport_Sendmail implements \Magento\Framework\Mail\TransportInterface +use Magento\Framework\Exception\MailException; +use Magento\Framework\Phrase; +use Zend\Mail\Message as ZendMessage; +use Zend\Mail\Transport\Sendmail; + +class Transport implements \Magento\Framework\Mail\TransportInterface { /** - * @var \Magento\Framework\Mail\MessageInterface + * @var Sendmail + */ + private $zendTransport; + + /** + * @var Message */ - protected $_message; + private $message; /** * @param MessageInterface $message - * @param null $parameters - * @throws \InvalidArgumentException + * @param null|string|array|\Traversable $parameters */ - public function __construct(\Magento\Framework\Mail\MessageInterface $message, $parameters = null) + public function __construct(MessageInterface $message, $parameters = null) { - if (!$message instanceof \Zend_Mail) { - throw new \InvalidArgumentException('The message should be an instance of \Zend_Mail'); - } - parent::__construct($parameters); - $this->_message = $message; + $this->zendTransport = new Sendmail($parameters); + $this->message = $message; } /** - * Send a mail using this transport - * - * @return void - * @throws \Magento\Framework\Exception\MailException + * @inheritdoc */ public function sendMessage() { try { - parent::send($this->_message); + $this->zendTransport->send( + ZendMessage::fromString($this->message->getRawMessage()) + ); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\MailException(new \Magento\Framework\Phrase($e->getMessage()), $e); + throw new MailException(new Phrase($e->getMessage()), $e); } } @@ -47,6 +51,6 @@ public function sendMessage() */ public function getMessage() { - return $this->_message; + return $this->message; } } diff --git a/lib/internal/Magento/Framework/Mview/View.php b/lib/internal/Magento/Framework/Mview/View.php index 1f1bde6bb021c..fca827ca2505f 100644 --- a/lib/internal/Magento/Framework/Mview/View.php +++ b/lib/internal/Magento/Framework/Mview/View.php @@ -285,7 +285,7 @@ public function update() for ($versionFrom = $lastVersionId; $versionFrom < $currentVersionId; $versionFrom += $versionBatchSize) { // Don't go past the current version for atomicy. $versionTo = min($currentVersionId, $versionFrom + $versionBatchSize); - $ids = $this->getChangelog()->getList($versionFrom, $versionTo); + $ids = array_map('intval', $this->getChangelog()->getList($versionFrom, $versionTo)); // We run the actual indexer in batches. Chunked AFTER loading to avoid duplicates in separate chunks. $chunks = array_chunk($ids, $batchSize); diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php index 0aaf67b5c7cfc..39cbea74847f9 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php @@ -165,7 +165,7 @@ protected function _getMethodInfo(\ReflectionMethod $method) 'parameters' => $parameters, 'body' => $this->_getMethodBody($method->getName(), $parameterNames), 'docblock' => ['shortDescription' => '{@inheritdoc}'], - 'returnType' => $method->getReturnType(), + 'returnType' => $this->getReturnTypeValue($method->getReturnType()), ]; return $methodInfo; @@ -245,4 +245,23 @@ protected function _validateData() } return $result; } + + /** + * Returns return type + * + * @param mixed $returnType + * @return null|string + */ + private function getReturnTypeValue($returnType) + { + $returnTypeValue = null; + + if ($returnType) { + $returnTypeValue = ((string)$returnType === 'self') + ? $this->getSourceClassName() + : (string)$returnType; + } + + return $returnTypeValue; + } } diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php index ab1bca9f4548c..43bfd46c1193a 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php @@ -104,13 +104,13 @@ protected function _toHtml() if ($this->isCurrent()) { $html = '<li class="nav item current">'; $html .= '<strong>' - . $this->escapeHtml((string)new \Magento\Framework\Phrase($this->getLabel())) + . $this->escapeHtml(__($this->getLabel())) . '</strong>'; $html .= '</li>'; } else { $html = '<li class="nav item' . $highlight . '"><a href="' . $this->escapeHtml($this->getHref()) . '"'; $html .= $this->getTitle() - ? ' title="' . $this->escapeHtml((string)new \Magento\Framework\Phrase($this->getTitle())) . '"' + ? ' title="' . $this->escapeHtml(__($this->getTitle())) . '"' : ''; $html .= $this->getAttributesHtml() . '>'; @@ -118,7 +118,7 @@ protected function _toHtml() $html .= '<strong>'; } - $html .= $this->escapeHtml((string)new \Magento\Framework\Phrase($this->getLabel())); + $html .= $this->escapeHtml(__($this->getLabel())); if ($this->getIsHighlighted()) { $html .= '</strong>'; diff --git a/lib/web/css/source/lib/_sections.less b/lib/web/css/source/lib/_sections.less index 372008d061580..b73e317835955 100644 --- a/lib/web/css/source/lib/_sections.less +++ b/lib/web/css/source/lib/_sections.less @@ -49,16 +49,16 @@ @_tab-control-color: @tab-control__color, @_tab-control-text-decoration: @tab-control__text-decoration, - @_tab-control-color-visited: @tab-control__color, - @_tab-control-text-decoration-visited: @tab-control__text-decoration, + @_tab-control-color-visited: @tab-control__visited__color, + @_tab-control-text-decoration-visited: @tab-control__visited__text-decoration, @_tab-control-background-color-hover: @tab-control__hover__background-color, @_tab-control-color-hover: @tab-control__hover__color, - @_tab-control-text-decoration-hover: @tab-control__text-decoration, + @_tab-control-text-decoration-hover: @tab-control__hover__text-decoration, @_tab-control-background-color-active: @tab-control__active__background-color, @_tab-control-color-active: @tab-control__active__color, - @_tab-control-text-decoration-active: @tab-control__text-decoration, + @_tab-control-text-decoration-active: @tab-control__active__text-decoration, @_tab-control-height: @tab-control__height, @_tab-control-margin-right: @tab-control__margin-right, @@ -121,6 +121,7 @@ &.active > .switch:hover { .lib-css(background, @_tab-control-background-color-active); .lib-css(color, @_tab-control-color-active); + .lib-css(text-decoration, @_tab-control-text-decoration-active); } &.active > .switch, @@ -200,8 +201,8 @@ @_accordion-control-color: @accordion-control__color, @_accordion-control-text-decoration: @accordion-control__text-decoration, - @_accordion-control-color-visited: @accordion-control__color, - @_accordion-control-text-decoration-visited: @accordion-control__text-decoration, + @_accordion-control-color-visited: @accordion-control__visited__color, + @_accordion-control-text-decoration-visited: @accordion-control__visited__text-decoration, @_accordion-control-background-color-hover: @accordion-control__hover__background-color, @_accordion-control-color-hover: @accordion-control__hover__color, @@ -275,6 +276,8 @@ &.active > .switch:focus, &.active > .switch:hover { .lib-css(background, @_accordion-control-background-color-active); + .lib-css(color, @_accordion-control-color-active); + .lib-css(text-decoration, @_accordion-control-text-decoration-active); .lib-css(padding-bottom, @_accordion-control-padding-bottom); } } diff --git a/lib/web/css/source/lib/variables/_sections.less b/lib/web/css/source/lib/variables/_sections.less index 7d961077e6f59..05226d8aa3a3c 100644 --- a/lib/web/css/source/lib/variables/_sections.less +++ b/lib/web/css/source/lib/variables/_sections.less @@ -40,6 +40,9 @@ @tab-control__active__color: @text__color; @tab-control__active__text-decoration: @tab-control__text-decoration; +@tab-control__visited__color: @tab-control__color; +@tab-control__visited__text-decoration: @tab-control__text-decoration; + @tab-content__background-color: @tab-control__active__background-color; @tab-content__border-top-status: false; @tab-content__border: @tab-control__border-width solid @tab-control__border-color; @@ -72,8 +75,8 @@ @accordion-control__padding-bottom: @tab-control__padding-bottom; @accordion-control__padding-left: @accordion-control__padding-right; -@accordion-control__visited__color: @accordion-control__color; -@accordion-control__visited__text-decoration: @accordion-control__text-decoration; +@accordion-control__visited__color: @tab-control__visited__color; +@accordion-control__visited__text-decoration: @tab-control__visited__text-decoration; @accordion-control__hover__background-color: @tab-control__hover__background-color; @accordion-control__hover__color: @tab-control__hover__color; diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 7f54002bf499c..4062c501cb798 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -2092,7 +2092,7 @@ fotoramaVersion = '4.6.4'; o_navTop = opts.navposition === 'top'; classes.remove.push(selectClass); - $arrs.toggle(opts.arrows); + $arrs.toggle(!!opts.arrows); } else { o_nav = false; $arrs.hide(); diff --git a/lib/web/fotorama/fotorama.min.js b/lib/web/fotorama/fotorama.min.js index ea38823c4305b..e8eb9fbda63ef 100644 --- a/lib/web/fotorama/fotorama.min.js +++ b/lib/web/fotorama/fotorama.min.js @@ -1,4 +1,4 @@ /*! * Fotorama 4.6.4 | http://fotorama.io/license/ */ -fotoramaVersion="4.6.4";(function(bo,k,a3,bV,aP){var ag="fotorama",bH="fotorama__fullscreen",ae=ag+"__wrap",ah=ae+"--css2",aX=ae+"--css3",bt=ae+"--video",ar=ae+"--fade",aw=ae+"--slide",P=ae+"--no-controls",aM=ae+"--no-shadows",U=ae+"--pan-y",a0=ae+"--rtl",az=ae+"--only-active",bN=ae+"--no-captions",f=ae+"--toggle-arrows",a7=ag+"__stage",x=a7+"__frame",l=x+"--video",B=a7+"__shaft",aB=ag+"__grab",bC=ag+"__pointer",aK=ag+"__arr",F=aK+"--disabled",bc=aK+"--prev",r=aK+"--next",bO=ag+"__nav",bq=bO+"-wrap",aH=bO+"__shaft",b=bq+"--vertical",ax=bq+"--list",bZ=bq+"--horizontal",bW=bO+"--dots",ai=bO+"--thumbs",aG=bO+"__frame",br=ag+"__fade",al=br+"-front",n=br+"-rear",aW=ag+"__shadow",bz=aW+"s",S=bz+"--left",aL=bz+"--right",a2=bz+"--top",aR=bz+"--bottom",a4=ag+"__active",a9=ag+"__select",bs=ag+"--hidden",M=ag+"--fullscreen",aJ=ag+"__fullscreen-icon",bP=ag+"__error",bM=ag+"__loading",c=ag+"__loaded",b3=c+"--full",bg=c+"--img",bR=ag+"__grabbing",J=ag+"__img",Y=J+"--full",bS=ag+"__thumb",b0=bS+"__arr--left",H=bS+"__arr--right",cb=bS+"-border",bd=ag+"__html",af=ag+"-video-container",bJ=ag+"__video",T=bJ+"-play",w=bJ+"-close",au=ag+"_horizontal_ratio",aY=ag+"_vertical_ratio",ca=ag+"__spinner",Z=ca+"--show";var E=bV&&bV.fn.jquery.split(".");if(!E||E[0]<1||(E[0]==1&&E[1]<8)){throw"Fotorama requires jQuery 1.8 or later and will not run without it."}var bx={};var ap=(function(co,ct,cj){var cf="2.8.3",cm={},cD=ct.documentElement,cE="modernizr",cB=ct.createElement(cE),cp=cB.style,cg,cw={}.toString,cy=" -webkit- -moz- -o- -ms- ".split(" "),cd="Webkit Moz O ms",cG=cd.split(" "),cq=cd.toLowerCase().split(" "),ck={},ce={},cu={},cA=[],cv=cA.slice,cc,cz=function(cQ,cS,cK,cR){var cJ,cP,cM,cN,cI=ct.createElement("div"),cO=ct.body,cL=cO||ct.createElement("body");if(parseInt(cK,10)){while(cK--){cM=ct.createElement("div");cM.id=cR?cR[cK]:cE+(cK+1);cI.appendChild(cM)}}cJ=["­",'<style id="s',cE,'">',cQ,"</style>"].join("");cI.id=cE;(cO?cI:cL).innerHTML+=cJ;cL.appendChild(cI);if(!cO){cL.style.background="";cL.style.overflow="hidden";cN=cD.style.overflow;cD.style.overflow="hidden";cD.appendChild(cL)}cP=cS(cI,cQ);if(!cO){cL.parentNode.removeChild(cL);cD.style.overflow=cN}else{cI.parentNode.removeChild(cI)}return !!cP},cs=({}).hasOwnProperty,cC;if(!cl(cs,"undefined")&&!cl(cs.call,"undefined")){cC=function(cI,cJ){return cs.call(cI,cJ)}}else{cC=function(cI,cJ){return((cJ in cI)&&cl(cI.constructor.prototype[cJ],"undefined"))}}if(!Function.prototype.bind){Function.prototype.bind=function cH(cK){var cL=this;if(typeof cL!="function"){throw new TypeError()}var cI=cv.call(arguments,1),cJ=function(){if(this instanceof cJ){var cO=function(){};cO.prototype=cL.prototype;var cN=new cO();var cM=cL.apply(cN,cI.concat(cv.call(arguments)));if(Object(cM)===cM){return cM}return cN}else{return cL.apply(cK,cI.concat(cv.call(arguments)))}};return cJ}}function cr(cI){cp.cssText=cI}function ci(cJ,cI){return cr(cy.join(cJ+";")+(cI||""))}function cl(cJ,cI){return typeof cJ===cI}function cn(cJ,cI){return !!~(""+cJ).indexOf(cI)}function cF(cK,cI){for(var cJ in cK){var cL=cK[cJ];if(!cn(cL,"-")&&cp[cL]!==cj){return cI=="pfx"?cL:true}}return false}function cx(cJ,cM,cL){for(var cI in cJ){var cK=cM[cJ[cI]];if(cK!==cj){if(cL===false){return cJ[cI]}if(cl(cK,"function")){return cK.bind(cL||cM)}return cK}}return false}function i(cM,cI,cL){var cJ=cM.charAt(0).toUpperCase()+cM.slice(1),cK=(cM+" "+cG.join(cJ+" ")+cJ).split(" ");if(cl(cI,"string")||cl(cI,"undefined")){return cF(cK,cI)}else{cK=(cM+" "+(cq).join(cJ+" ")+cJ).split(" ");return cx(cK,cI,cL)}}ck.touch=function(){var cI;if(("ontouchstart" in co)||co.DocumentTouch&&ct instanceof DocumentTouch){cI=true}else{cz(["@media (",cy.join("touch-enabled),("),cE,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(cJ){cI=cJ.offsetTop===9})}return cI};ck.csstransforms3d=function(){var cI=!!i("perspective");if(cI&&"webkitPerspective" in cD.style){cz("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(cJ,cK){cI=cJ.offsetLeft===9&&cJ.offsetHeight===3})}return cI};ck.csstransitions=function(){return i("transition")};for(var ch in ck){if(cC(ck,ch)){cc=ch.toLowerCase();cm[cc]=ck[ch]();cA.push((cm[cc]?"":"no-")+cc)}}cm.addTest=function(cJ,cK){if(typeof cJ=="object"){for(var cI in cJ){if(cC(cJ,cI)){cm.addTest(cI,cJ[cI])}}}else{cJ=cJ.toLowerCase();if(cm[cJ]!==cj){return cm}cK=typeof cK=="function"?cK():cK;if(typeof enableClasses!=="undefined"&&enableClasses){cD.className+=" "+(cK?"":"no-")+cJ}cm[cJ]=cK}return cm};cr("");cB=cg=null;cm._version=cf;cm._prefixes=cy;cm._domPrefixes=cq;cm._cssomPrefixes=cG;cm.testProp=function(cI){return cF([cI])};cm.testAllProps=i;cm.testStyles=cz;cm.prefixed=function(cK,cJ,cI){if(!cJ){return i(cK,"pfx")}else{return i(cK,cJ,cI)}};return cm})(bo,k);var bB={ok:false,is:function(){return false},request:function(){},cancel:function(){},event:"",prefix:""},h="webkit moz o ms khtml".split(" ");if(typeof k.cancelFullScreen!="undefined"){bB.ok=true}else{for(var bv=0,N=h.length;bv<N;bv++){bB.prefix=h[bv];if(typeof k[bB.prefix+"CancelFullScreen"]!="undefined"){bB.ok=true;break}}}if(bB.ok){bB.event=bB.prefix+"fullscreenchange";bB.is=function(){switch(this.prefix){case"":return k.fullScreen;case"webkit":return k.webkitIsFullScreen;default:return k[this.prefix+"FullScreen"]}};bB.request=function(i){return(this.prefix==="")?i.requestFullScreen():i[this.prefix+"RequestFullScreen"]()};bB.cancel=function(i){return(this.prefix==="")?k.cancelFullScreen():k[this.prefix+"CancelFullScreen"]()}}function a6(i){var cc="bez_"+bV.makeArray(arguments).join("_").replace(".","p");if(typeof bV.easing[cc]!=="function"){var cd=function(ck,ci){var cf=[null,null],cl=[null,null],cj=[null,null],ch=function(cm,cn){cj[cn]=3*ck[cn];cl[cn]=3*(ci[cn]-ck[cn])-cj[cn];cf[cn]=1-cj[cn]-cl[cn];return cm*(cj[cn]+cm*(cl[cn]+cm*cf[cn]))},cg=function(cm){return cj[0]+cm*(2*cl[0]+3*cf[0]*cm)},ce=function(co){var cm=co,cn=0,cp;while(++cn<14){cp=ch(cm,0)-co;if(Math.abs(cp)<0.001){break}cm-=cp/cg(cm)}return cm};return function(cm){return ch(ce(cm),1)}};bV.easing[cc]=function(cf,cg,ce,ci,ch){return ci*cd([i[0],i[1]],[i[2],i[3]])(cg/ch)+ce}}return cc}var bf=bV(bo),bw=bV(k),R,I,bT=a3.hash.replace("#","")==="quirks",ac=ap.csstransforms3d,aA=ac&&!bT,aN=ac||k.compatMode==="CSS1Compat",s=bB.ok,am=navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i),bh=!aA||am,aZ=navigator.msPointerEnabled,y="onwheel" in k.createElement("div")?"wheel":k.onmousewheel!==aP?"mousewheel":"DOMMouseScroll",b8=250,ba=300,bY=1400,bQ=5000,b9=2,L=64,bk=500,bE=333,bu="$stageFrame",b7="$navDotFrame",bl="$navThumbFrame",bF="auto",u=a6([0.1,0,0.25,1]),bK=1200,b4=1,Q={width:null,minwidth:null,maxwidth:"100%",height:null,minheight:null,maxheight:null,ratio:null,margin:b9,nav:"dots",navposition:"bottom",navwidth:null,thumbwidth:L,thumbheight:L,thumbmargin:b9,thumbborderwidth:b9,allowfullscreen:false,transition:"slide",clicktransition:null,transitionduration:ba,captions:true,startindex:0,loop:false,autoplay:false,stopautoplayontouch:true,keyboard:false,arrows:true,click:true,swipe:false,trackpad:false,shuffle:false,direction:"ltr",shadows:true,showcaption:true,navdir:"horizontal",navarrows:true,navtype:"thumbs"},p={left:true,right:true,down:true,up:true,space:false,home:false,end:false};function g(){}function bb(cd,cc,i){return Math.max(isNaN(cc)?-Infinity:cc,Math.min(isNaN(i)?Infinity:i,cd))}function bi(cc,i){return cc.match(/ma/)&&cc.match(/-?\d+(?!d)/g)[cc.match(/3d/)?(i==="vertical"?13:12):(i==="vertical"?5:4)]}function aa(cc,i){if(aA){return +bi(cc.css("transform"),i)}else{return +cc.css(i==="vertical"?"top":"left").replace("px","")}}function b2(cd,cc){var i={};if(aA){switch(cc){case"vertical":i.transform="translate3d(0, "+(cd)+"px,0)";break;case"list":break;default:i.transform="translate3d("+(cd)+"px,0,0)";break}}else{cc==="vertical"?i.top=cd:i.left=cd}return i}function b6(i){return{"transition-duration":i+"ms"}}function aV(cc,i){return isNaN(cc)?i:cc}function m(cc,i){return aV(+String(cc).replace(i||"px",""))}function K(i){return/%$/.test(i)?m(i,"%"):aP}function d(cc,i){return aV(K(cc)/100*i,m(cc))}function t(i){return(!isNaN(m(i))||!isNaN(m(i,"%")))&&i}function a8(cc,cd,ce,i){return(cc-(i||0))*(cd+(ce||0))}function by(ce,cc,cd,i){return -Math.round(ce/(cc+(cd||0))-(i||0))}function aO(cd){var cc=cd.data();if(cc.tEnd){return}var ce=cd[0],i={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",msTransition:"MSTransitionEnd",transition:"transitionend"};D(ce,i[ap.prefixed("transition")],function(cf){cc.tProp&&cf.propertyName.match(cc.tProp)&&cc.onEndFn()});cc.tEnd=true}function X(cd,cf,ce,cg){var cc,i=cd.data();if(i){i.onEndFn=function(){if(cc){return}cc=true;clearTimeout(i.tT);ce()};i.tProp=cf;clearTimeout(i.tT);i.tT=setTimeout(function(){i.onEndFn()},cg*1.5);aO(cd)}}function a1(cd,cf){var cc=cd.navdir||"horizontal";if(cd.length){var i=cd.data();if(aA){cd.css(b6(0));i.onEndFn=g;clearTimeout(i.tT)}else{cd.stop()}var ce=bj(cf,function(){return aa(cd,cc)});cd.css(b2(ce,cc));return ce}}function bj(){var cc;for(var cd=0,i=arguments.length;cd<i;cd++){cc=cd?arguments[cd]():arguments[cd];if(typeof cc==="number"){break}}return cc}function aF(cc,i){return Math.round(cc+((i-cc)/1.5))}function aU(){aU.p=aU.p||(a3.protocol==="https:"?"https://":"http://");return aU.p}function ak(cc){var i=k.createElement("a");i.href=cc;return i}function at(i,cd){if(typeof i!=="string"){return i}i=ak(i);var cf,ce;if(i.host.match(/youtube\.com/)&&i.search){cf=i.search.split("v=")[1];if(cf){var cc=cf.indexOf("&");if(cc!==-1){cf=cf.substring(0,cc)}ce="youtube"}}else{if(i.host.match(/youtube\.com|youtu\.be/)){cf=i.pathname.replace(/^\/(embed\/|v\/)?/,"").replace(/\/.*/,"");ce="youtube"}else{if(i.host.match(/vimeo\.com/)){ce="vimeo";cf=i.pathname.replace(/^\/(video\/)?/,"").replace(/\/.*/,"")}}}if((!cf||!ce)&&cd){cf=i.href;ce="custom"}return cf?{id:cf,type:ce,s:i.search.replace(/^\?/,""),p:aU()}:false}function aQ(cg,ce,cf){var cc,i,cd=cg.video;if(cd.type==="youtube"){i=aU()+"img.youtube.com/vi/"+cd.id+"/default.jpg";cc=i.replace(/\/default.jpg$/,"/hqdefault.jpg");cg.thumbsReady=true}else{if(cd.type==="vimeo"){bV.ajax({url:aU()+"vimeo.com/api/v2/video/"+cd.id+".json",dataType:"jsonp",success:function(ch){cg.thumbsReady=true;v(ce,{img:ch[0].thumbnail_large,thumb:ch[0].thumbnail_small},cg.i,cf)}})}else{cg.thumbsReady=true}}return{img:cc,thumb:i}}function v(ch,cd,cf,ci){for(var cg=0,ce=ch.length;cg<ce;cg++){var cj=ch[cg];if(cj.i===cf&&cj.thumbsReady){var cc={videoReady:true};cc[bu]=cc[bl]=cc[b7]=false;ci.splice(cg,1,bV.extend({},cj,cc,cd));break}}}function bI(cc){var ce=[];function cd(cf,ch,cn){var ci=cf.children("img").eq(0),cm=cf.attr("href"),ck=cf.attr("src"),cl=ci.attr("src"),cj=ch.video,cg=cn?at(cm,cj===true):false;if(cg){cm=false}else{cg=cj}i(cf,ci,bV.extend(ch,{video:cg,img:ch.img||cm||ck||cl,thumb:ch.thumb||cl||ck||cm}))}function i(cg,cj,ck){var ci=ck.thumb&&ck.img!==ck.thumb,ch=m(ck.width||cg.attr("width")),cf=m(ck.height||cg.attr("height"));bV.extend(ck,{width:ch,height:cf,thumbratio:bm(ck.thumbratio||(m(ck.thumbwidth||(cj&&cj.attr("width"))||ci||ch)/m(ck.thumbheight||(cj&&cj.attr("height"))||ci||cf)))})}cc.children().each(function(){var cf=bV(this),cg=bA(bV.extend(cf.data(),{id:cf.attr("id")}));if(cf.is("a, img")){cd(cf,cg,true)}else{if(!cf.is(":empty")){i(cf,null,bV.extend(cg,{html:this,_html:cf.html()}))}else{return}}ce.push(cg)});return ce}function W(i){return i.offsetWidth===0&&i.offsetHeight===0}function be(i){return !bV.contains(k.documentElement,i)}function bX(cf,cd,ce,cc){if(!bX.i){bX.i=1;bX.ii=[true]}cc=cc||bX.i;if(typeof bX.ii[cc]==="undefined"){bX.ii[cc]=true}if(cf()){cd()}else{bX.ii[cc]&&setTimeout(function(){bX.ii[cc]&&bX(cf,cd,ce,cc)},ce||100)}return bX.i++}bX.stop=function(cc){bX.ii[cc]=false};function V(ce,cd){var cc=ce.data(),cg=cc.measures;if(cg&&(!cc.l||cc.l.W!==cg.width||cc.l.H!==cg.height||cc.l.r!==cg.ratio||cc.l.w!==cd.w||cc.l.h!==cd.h)){var i=bb(cd.h,0,cg.height),cf=i*cg.ratio;aS.setRatio(ce,cf,i);cc.l={W:cg.width,H:cg.height,r:cg.ratio,w:cd.w,h:cd.h}}return true}function an(i,cd){var cc=i[0];if(cc.styleSheet){cc.styleSheet.cssText=cd}else{i.html(cd)}}function bp(ce,cd,i,cc){return cd===i?false:cc==="vertical"?(ce<=cd?"top":ce>=i?"bottom":"top bottom"):(ce<=cd?"left":ce>=i?"right":"left right")}function z(cc,cd,i){i=i||{};cc.each(function(){var cg=bV(this),cf=cg.data(),ce;if(cf.clickOn){return}cf.clickOn=true;bV.extend(aI(cg,{onStart:function(ch){ce=ch;(i.onStart||g).call(this,ch)},onMove:i.onMove||g,onTouchEnd:i.onTouchEnd||g,onEnd:function(ch){if(ch.moved){return}cd.call(this,ce)}}),{noMove:true})})}function ab(i,cc){return'<div class="'+i+'">'+(cc||"")+"</div>"}function aT(i){return"."+i}function q(i){var cc='<iframe src="'+i.p+i.type+".com/embed/"+i.id+'" frameborder="0" allowfullscreen></iframe>';return cc}function aC(cf){var cc=cf.length;while(cc){var ce=Math.floor(Math.random()*cc--);var cd=cf[cc];cf[cc]=cf[ce];cf[ce]=cd}return cf}function bG(i){return Object.prototype.toString.call(i)=="[object Array]"&&bV.map(i,function(cc){return bV.extend({},cc)})}function bU(i,cd,cc){i.scrollLeft(cd||0).scrollTop(cc||0)}function bA(i){if(i){var cc={};bV.each(i,function(cd,ce){cc[cd.toLowerCase()]=ce});return cc}}function bm(i){if(!i){return}var cc=+i;if(!isNaN(cc)){return cc}else{cc=i.split("/");return +cc[0]/+cc[1]||aP}}function D(cd,ce,cc,i){if(!ce){return}cd.addEventListener?cd.addEventListener(ce,cc,!!i):cd.attachEvent("on"+ce,cc)}function a5(i,cc){if(i>cc.max){i=cc.max}else{if(i<cc.min){i=cc.min}}return i}function aD(i,ck,ch,cf,ce,cd,cc){var cg,cj,ci;if(cc==="horizontal"){cj=i.thumbwidth;ci=cd.width()}else{cj=i.thumbheight;ci=cd.height()}if((cj+i.margin)*(ch+1)>=(ci-cf)){if(cc==="horizontal"){cg=-ce.position().left}else{cg=-ce.position().top}}else{if((cj+i.margin)*(ch)<=Math.abs(cf)){if(cc==="horizontal"){cg=-ce.position().left+ci-(cj+i.margin)}else{cg=-ce.position().top+ci-(cj+i.margin)}}else{cg=cf}}cg=a5(cg,ck);return cg||0}function aj(i){return !!i.getAttribute("disabled")}function ad(cc,i){if(i){return{disabled:cc}}else{return{tabindex:cc*-1+"",disabled:cc}}}function a(cc,i){D(cc,"keyup",function(cd){aj(cc)||cd.keyCode==13&&i.call(cc,cd)})}function bL(cc,i){D(cc,"focus",cc.onfocusin=function(cd){i.call(cc,cd)},true)}function O(cc,i){cc.preventDefault?cc.preventDefault():(cc.returnValue=false);i&&cc.stopPropagation&&cc.stopPropagation()}function aE(cd,cc){var i=/iP(ad|hone|od)/i.test(bo.navigator.userAgent);if(i&&cc==="touchend"){cd.on("touchend",function(ce){bw.trigger("mouseup",ce)})}cd.on(cc,function(ce){O(ce,true);return false})}function ay(i){return i?">":"<"}var aS=(function(){function cd(ch,ce,cg){var cf=ce/cg;if(cf<=1){ch.parent().removeClass(au);ch.parent().addClass(aY)}else{ch.parent().removeClass(aY);ch.parent().addClass(au)}}function i(cf,cg,ch){var ce=ch;if(!cf.attr(ce)&&cf.attr(ce)!==aP){cf.attr(ce,cg)}if(cf.find("["+ce+"]").length){cf.find("["+ce+"]").each(function(){bV(this).attr(ce,cg)})}}function cc(cf,ce,ci){var cg=false,ch;cf.showCaption===ci||cf.showCaption===true?ch=true:ch=false;if(!ce){return false}if(cf.caption&&ch){cg=true}return cg}return{setRatio:cd,setThumbAttr:i,isExpectedCaption:cc}}(aS||{},jQuery));function A(ce,cd){var cc=ce.data(),i=Math.round(cd.pos),cf=function(){if(cc&&cc.sliding){cc.sliding=false}(cd.onEnd||g)()};if(typeof cd.overPos!=="undefined"&&cd.overPos!==cd.pos){i=cd.overPos}var cg=bV.extend(b2(i,cd.direction),cd.width&&{width:cd.width},cd.height&&{height:cd.height});if(cc&&cc.sliding){cc.sliding=true}if(aA){ce.css(bV.extend(b6(cd.time),cg));if(cd.time>10){X(ce,"transform",cf,cd.time)}else{cf()}}else{ce.stop().animate(cg,cd.time,u,cf)}}function aq(ck,cj,cc,cm,ce,i){var ch=typeof i!=="undefined";if(!ch){ce.push(arguments);Array.prototype.push.call(arguments,ce.length);if(ce.length>1){return}}ck=ck||bV(ck);cj=cj||bV(cj);var ci=ck[0],cg=cj[0],cf=cm.method==="crossfade",cl=function(){if(!cl.done){cl.done=true;var cn=(ch||ce.shift())&&ce.shift();cn&&aq.apply(this,cn);(cm.onEnd||g)(!!cn)}},cd=cm.time/(i||1);cc.removeClass(n+" "+al);ck.stop().addClass(n);cj.stop().addClass(al);cf&&cg&&ck.fadeTo(0,0);ck.fadeTo(cf?cd:0,1,cf&&cl);cj.fadeTo(cd,0,cl);(ci&&cf)||cg||cl()}var G,b5,e,j,bD;function bn(i){var cc=(i.touches||[])[0]||i;i._x=cc.pageX||cc.originalEvent.pageX;i._y=cc.clientY||cc.originalEvent.clientY;i._now=bV.now()}function aI(cr,cg){var cc=cr[0],cj={},i,cl,cf,cn,cs,cd,ce,co,ch;function cq(ct){cf=bV(ct.target);cj.checked=cd=ce=ch=false;if(i||cj.flow||(ct.touches&&ct.touches.length>1)||ct.which>1||(G&&G.type!==ct.type&&e)||(cd=cg.select&&cf.is(cg.select,cc))){return cd}cs=ct.type==="touchstart";ce=cf.is("a, a *",cc);cn=cj.control;co=(cj.noMove||cj.noSwipe||cn)?16:!cj.snap?4:0;bn(ct);cl=G=ct;b5=ct.type.replace(/down|start/,"move").replace(/Down/,"Move");(cg.onStart||g).call(cc,ct,{control:cn,$target:cf});i=cj.flow=true;if(!cs||cj.go){O(ct)}}function ck(cx){if((cx.touches&&cx.touches.length>1)||(aZ&&!cx.isPrimary)||b5!==cx.type||!i){i&&ci();(cg.onTouchEnd||g)();return}bn(cx);var cy=Math.abs(cx._x-cl._x),cu=Math.abs(cx._y-cl._y),cw=cy-cu,cv=(cj.go||cj.x||cw>=0)&&!cj.noSwipe,ct=cw<0;if(cs&&!cj.checked){if(i=cv){O(cx)}}else{O(cx);(cg.onMove||g).call(cc,cx,{touch:cs})}if(!ch&&Math.sqrt(Math.pow(cy,2)+Math.pow(cu,2))>co){ch=true}cj.checked=cj.checked||cv||ct}function ci(cu){(cg.onTouchEnd||g)();var ct=i;cj.control=i=false;if(ct){cj.flow=false}if(!ct||(ce&&!cj.checked)){return}cu&&O(cu);e=true;clearTimeout(j);j=setTimeout(function(){e=false},1000);(cg.onEnd||g).call(cc,{moved:ch,$target:cf,control:cn,touch:cs,startEvent:cl,aborted:!cu||cu.type==="MSPointerCancel"})}function cm(){if(cj.flow){return}cj.flow=true}function cp(){if(!cj.flow){return}cj.flow=false}if(aZ){D(cc,"MSPointerDown",cq);D(k,"MSPointerMove",ck);D(k,"MSPointerCancel",ci);D(k,"MSPointerUp",ci)}else{D(cc,"touchstart",cq);D(cc,"touchmove",ck);D(cc,"touchend",ci);D(k,"touchstart",cm);D(k,"touchend",cp);D(k,"touchcancel",cp);bf.on("scroll",cp);cr.on("mousedown pointerdown",cq);bw.on("mousemove pointermove",ck).on("mouseup pointerup",ci)}if(ap.touch){bD="a"}else{bD="div"}cr.on("click",bD,function(ct){cj.checked&&O(ct)});return cj}function ao(cz,cd){var cc=cz[0],ce=cz.data(),cm={},cw,cf,cx,cj,ch,cy,co,cg,cr,ct,cp,cq,i,cv,ci,cn;function cs(cA,cB){cn=true;cw=cf=(cq==="vertical")?cA._y:cA._x;co=cA._now;cy=[[co,cw]];cx=cj=cm.noMove||cB?0:a1(cz,(cd.getPos||g)());(cd.onStart||g).call(cc,cA)}function cu(cB,cA){cr=cm.min;ct=cm.max;cp=cm.snap,cq=cm.direction||"horizontal",cz.navdir=cq;i=cB.altKey;cn=ci=false;cv=cA.control;if(!cv&&!ce.sliding){cs(cB)}}function cl(cB,cA){if(!cm.noSwipe){if(!cn){cs(cB)}cf=(cq==="vertical")?cB._y:cB._x;cy.push([cB._now,cf]);cj=cx-(cw-cf);ch=bp(cj,cr,ct,cq);if(cj<=cr){cj=aF(cj,cr)}else{if(cj>=ct){cj=aF(cj,ct)}}if(!cm.noMove){cz.css(b2(cj,cq));if(!ci){ci=true;cA.touch||aZ||cz.addClass(bR)}(cd.onMove||g).call(cc,cB,{pos:cj,edge:ch})}}}function ck(cJ){if(cm.noSwipe&&cJ.moved){return}if(!cn){cs(cJ.startEvent,true)}cJ.touch||aZ||cz.removeClass(bR);cg=bV.now();var cG=cg-b8,cK,cP,cQ,cS=null,cA,cE,cN,cD,cF,cI=ba,cO,cH=cd.friction;for(var cC=cy.length-1;cC>=0;cC--){cK=cy[cC][0];cP=Math.abs(cK-cG);if(cS===null||cP<cQ){cS=cK;cA=cy[cC][1]}else{if(cS===cG||cP>cQ){break}}cQ=cP}cD=bb(cj,cr,ct);var cT=cA-cf,cR=cT>=0,cL=cg-cS,cB=cL>b8,cM=!cB&&cj!==cx&&cD===cj;if(cp){cD=bb(Math[cM?(cR?"floor":"ceil"):"round"](cj/cp)*cp,cr,ct);cr=ct=cD}if(cM&&(cp||cD===cj)){cO=-(cT/cL);cI*=bb(Math.abs(cO),cd.timeLow,cd.timeHigh);cE=Math.round(cj+cO*cI/cH);if(!cp){cD=cE}if(!cR&&cE>ct||cR&&cE<cr){cN=cR?cr:ct;cF=cE-cN;if(!cp){cD=cN}cF=bb(cD+cF*0.03,cN-50,cN+50);cI=Math.abs((cj-cF)/(cO/cH))}}cI*=i?10:1;(cd.onEnd||g).call(cc,bV.extend(cJ,{moved:cJ.moved||cB&&cp,pos:cj,newPos:cD,overPos:cF,time:cI,dir:cq}))}cm=bV.extend(aI(cd.$wrap,bV.extend({},cd,{onStart:cu,onMove:cl,onEnd:ck})),cm);return cm}function o(ce,cd){var cg=ce[0],ch,cf,i,cc={prevent:{}};D(cg,y,function(co){var cl=co.wheelDeltaY||-1*co.deltaY||0,cn=co.wheelDeltaX||-1*co.deltaX||0,ck=Math.abs(cn)&&!Math.abs(cl),cm=ay(cn<0),cp=cf===cm,ci=bV.now(),cj=ci-i<b8;cf=cm;i=ci;if(!ck||!cc.ok||cc.prevent[cm]&&!ch){return}else{O(co,true);if(ch&&cp&&cj){return}}if(cd.shift){ch=true;clearTimeout(cc.t);cc.t=setTimeout(function(){ch=false},bY)}(cd.onEnd||g)(co,cd.shift?cm:cn)});return cc}jQuery.Fotorama=function(d6,c3){R=bV("html");I=bV("body");var cg=this,cC=bV.now(),cQ=ag+cC,eu=d6[0],dP,cY=1,cR=d6.data(),c1,de=bV("<style></style>"),c9=bV(ab(bs)),dk=d6.find(aT(ae)),cf=dk.find(aT(a7)),dY=cf[0],cl=d6.find(aT(B)),c8=bV(),dW=d6.find(aT(bc)),da=d6.find(aT(r)),cU=d6.find(aT(aK)),dU=d6.find(aT(bq)),dO=dU.find(aT(bO)),cF=dO.find(aT(aH)),dA,cB=bV(),cW=bV(),dS=cl.data(),cX=cF.data(),c7=d6.find(aT(cb)),eg=d6.find(aT(b0)),dX=d6.find(aT(H)),dM=d6.find(aT(aJ)),dD=dM[0],cH=bV(ab(T)),dt=d6.find(aT(w)),d1=dt[0],eb=d6.find(aT(ca)),dg,eo=false,dF,ea,c2,ed,dw,d4,cN,cK,dx,dj,cq,c0,d8,c4,d2,cv,ch,ej,ds,cu,ec,dH,dE,d0={},en={},dG,d5={},cG={},dy={},ef={},cs,cT,ee,cj,el,cd={},er={},dZ,c6,dz,dr,d3=0,cI=[];dk[bu]=bV('<div class="'+x+'"></div>');dk[bl]=bV(bV.Fotorama.jst.thumb());dk[b7]=bV(bV.Fotorama.jst.dots());cd[bu]=[];cd[bl]=[];cd[b7]=[];er[bu]={};dk.addClass(aA?aX:ah);cR.fotorama=this;function ep(){bV.each(dP,function(ey,eA){if(!eA.i){eA.i=cY++;var ez=at(eA.video,true);if(ez){var ex={};eA.video=ez;if(!eA.img&&!eA.thumb){ex=aQ(eA,dP,cg)}else{eA.thumbsReady=true}v(dP,{img:ex.img,thumb:ex.thumb},eA.i,cg)}}})}function df(ex){return dE[ex]}function i(){if(cf!==aP){if(c3.navdir=="vertical"){var ex=c3.thumbwidth+c3.thumbmargin;cf.css("left",ex);da.css("right",ex);dM.css("right",ex);dk.css("width",dk.css("width")+ex);cl.css("max-width",dk.width()-ex)}else{cf.css("left","");da.css("right","");dM.css("right","");dk.css("width",dk.css("width")+ex);cl.css("max-width","")}}}function ek(eB){var eC="keydown."+ag,eD=ag+cC,ex="keydown."+eD,eA="keyup."+eD,ey="resize."+eD+" orientationchange."+eD,ez;if(eB){bw.on(ex,function(eG){var eF,eE;if(dg&&eG.keyCode===27){eF=true;cO(dg,true,true)}else{if(cg.fullScreen||(c3.keyboard&&!cg.index)){if(eG.keyCode===27){eF=true;cg.cancelFullScreen()}else{if((eG.shiftKey&&eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===37&&df("left"))||(eG.keyCode===38&&df("up")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE="<"}else{if((eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===39&&df("right"))||(eG.keyCode===40&&df("down")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE=">"}else{if(eG.keyCode===36&&df("home")){cg.longPress.progress();eE="<<"}else{if(eG.keyCode===35&&df("end")){cg.longPress.progress();eE=">>"}}}}}}}(eF||eE)&&O(eG);ez={index:eE,slow:eG.altKey,user:true};eE&&(cg.longPress.inProgress?cg.showWhileLongPress(ez):cg.show(ez))});if(eB){bw.on(eA,function(eE){if(cg.longPress.inProgress){cg.showEndLongPress({user:true})}cg.longPress.reset()})}if(!cg.index){bw.off(eC).on(eC,"textarea, input, select",function(eE){!I.hasClass(bH)&&eE.stopPropagation()})}bf.on(ey,cg.resize)}else{bw.off(ex);bf.off(ey)}}function dd(ex){if(ex===dd.f){return}if(ex){d6.addClass(ag+" "+cQ).before(c9).before(de);C(cg)}else{c9.detach();de.detach();d6.html(cR.urtext).removeClass(cQ);av(cg)}ek(ex);dd.f=ex}function dn(){dP=cg.data=dP||bG(c3.data)||bI(d6);c1=cg.size=dP.length;eq.ok&&c3.shuffle&&aC(dP);ep();eo=cn(eo);c1&&dd(true)}function em(){var ex=c1<2||dg;d5.noMove=ex||cv;d5.noSwipe=ex||!c3.swipe;!cu&&cl.toggleClass(aB,!c3.click&&!d5.noMove&&!d5.noSwipe);aZ&&dk.toggleClass(U,!d5.noSwipe)}function dq(ex){if(ex===true){ex=""}c3.autoplay=Math.max(+ex||bQ,ds*1.5)}function db(ex){if(ex.navarrows&&ex.nav==="thumbs"){eg.show();dX.show()}else{eg.hide();dX.hide()}}function ck(ex,ey){return Math.floor(dk.width()/(ey.thumbwidth+ey.thumbmargin))}function dQ(){if(!c3.nav||c3.nav==="dots"){c3.navdir="horizontal"}cg.options=c3=bA(c3);b4=ck(dk,c3);cv=(c3.transition==="crossfade"||c3.transition==="dissolve");dj=c3.loop&&(c1>2||(cv&&(!cu||cu!=="slide")));ds=+c3.transitionduration||ba;dH=c3.direction==="rtl";dE=bV.extend({},c3.keyboard&&p,c3.keyboard);db(c3);var ey={add:[],remove:[]};function ex(ez,eA){ey[ez?"add":"remove"].push(eA)}if(c1>1){cq=c3.nav;d8=c3.navposition==="top";ey.remove.push(a9);cU.toggle(c3.arrows)}else{cq=false;cU.hide()}dh();cJ();ev();if(c3.autoplay){dq(c3.autoplay)}ch=m(c3.thumbwidth)||L;ej=m(c3.thumbheight)||L;cG.ok=ef.ok=c3.trackpad&&!bh;em();dL(c3,[en]);c0=cq==="thumbs";if(dU.filter(":hidden")&&!!cq){dU.show()}if(c0){dl(c1,"navThumb");dA=cW;dr=bl;an(de,bV.Fotorama.jst.style({w:ch,h:ej,b:c3.thumbborderwidth,m:c3.thumbmargin,s:cC,q:!aN}));dO.addClass(ai).removeClass(bW)}else{if(cq==="dots"){dl(c1,"navDot");dA=cB;dr=b7;dO.addClass(bW).removeClass(ai)}else{dU.hide();cq=false;dO.removeClass(ai+" "+bW)}}if(cq){if(d8){dU.insertBefore(cf)}else{dU.insertAfter(cf)}cz.nav=false;cz(dA,cF,"nav")}c4=c3.allowfullscreen;if(c4){dM.prependTo(cf);d2=s&&c4==="native";aE(dM,"touchend")}else{dM.detach();d2=false}ex(cv,ar);ex(!cv,aw);ex(!c3.captions,bN);ex(dH,a0);ex(c3.arrows,f);ec=c3.shadows&&!bh;ex(!ec,aM);dk.addClass(ey.add.join(" ")).removeClass(ey.remove.join(" "));d0=bV.extend({},c3);i()}function cZ(ex){return ex<0?(c1+(ex%c1))%c1:ex>=c1?ex%c1:ex}function cn(ex){return bb(ex,0,c1-1)}function du(ex){return dj?cZ(ex):cn(ex)}function dB(ex){return ex>0||dj?ex-1:false}function ci(ex){return ex<c1-1||dj?ex+1:false}function d9(){d5.min=dj?-Infinity:-a8(c1-1,en.w,c3.margin,c2);d5.max=dj?Infinity:-a8(0,en.w,c3.margin,c2);d5.snap=en.w+c3.margin}function c5(){var ex=(c3.navdir==="vertical");var ez=ex?cF.height():cF.width();var ey=ex?en.h:en.nw;dy.min=Math.min(0,ey-ez);dy.max=0;dy.direction=c3.navdir;cF.toggleClass(aB,!(dy.noMove=dy.min===dy.max))}function dm(ey,eA,ez){if(typeof ey==="number"){ey=new Array(ey);var ex=true}return bV.each(ey,function(eD,eB){if(ex){eB=eD}if(typeof eB==="number"){var eF=dP[cZ(eB)];if(eF){var eC="$"+eA+"Frame",eE=eF[eC];ez.call(this,eD,eB,eF,eE,eC,eE&&eE.data())}}})}function cc(eA,ex,ez,ey){if(!dG||(dG==="*"&&ey===dx)){eA=t(c3.width)||t(eA)||bk;ex=t(c3.height)||t(ex)||bE;cg.resize({width:eA,ratio:c3.ratio||ez||eA/ex},0,ey!==dx&&"*")}}function cx(ex,ey,eA,ez){dm(ex,ey,function(eM,eE,eD,eC,eR,eB){if(!eC){return}var eN=cg.fullScreen&&!eB.$full&&ey==="stage";if(eB.$img&&!ez&&!eN){return}var eS=new Image(),eG=bV(eS),eO=eG.data();eB[eN?"$full":"$img"]=eG;var eJ=ey==="stage"?(eN?"full":"img"):"thumb",eF=eD[eJ],eP=eN?eD.img:eD[ey==="stage"?"thumb":"img"];if(ey==="navThumb"){eC=eB.$wrap}function eH(eT){var eU=cZ(eE);dc(eT,{index:eU,src:eF,frame:dP[eU]})}function eK(){eG.remove();bV.Fotorama.cache[eF]="error";if((!eD.html||ey!=="stage")&&eP&&eP!==eF){eD[eJ]=eF=eP;eB.$full=null;cx([eE],ey,eA,true)}else{if(eF&&!eD.html&&!eN){eC.trigger("f:error").removeClass(bM).addClass(bP);eH("error")}else{if(ey==="stage"){eC.trigger("f:load").removeClass(bM+" "+bP).addClass(c);eH("load");cc()}}eB.state="error";if(c1>1&&dP[eE]===eD&&!eD.html&&!eD.deleted&&!eD.video&&!eN){eD.deleted=true;cg.splice(eE,1)}}}function eL(){bV.Fotorama.measures[eF]=eO.measures=bV.Fotorama.measures[eF]||{width:eS.width,height:eS.height,ratio:eS.width/eS.height};cc(eO.measures.width,eO.measures.height,eO.measures.ratio,eE);eG.off("load error").addClass(""+(eN?Y:J)).attr("aria-hidden","false").prependTo(eC);if(eC.hasClass(x)&&!eC.hasClass(af)){eC.attr("href",eG.attr("src"))}V(eG,(bV.isFunction(eA)?eA():eA)||en);bV.Fotorama.cache[eF]=eB.state="loaded";setTimeout(function(){eC.trigger("f:load").removeClass(bM+" "+bP).addClass(c+" "+(eN?b3:bg));if(ey==="stage"){eH("load")}else{if(eD.thumbratio===bF||!eD.thumbratio&&c3.thumbratio===bF){eD.thumbratio=eO.measures.ratio;dV()}}},0)}if(!eF){eK();return}function eI(){var eT=10;bX(function(){return !c6||!eT--&&!bh},function(){eL()})}if(!bV.Fotorama.cache[eF]){bV.Fotorama.cache[eF]="*";eG.on("load",eI).on("error",eK)}else{(function eQ(){if(bV.Fotorama.cache[eF]==="error"){eK()}else{if(bV.Fotorama.cache[eF]==="loaded"){setTimeout(eI,0)}else{setTimeout(eQ,100)}}})()}eB.state="";eS.src=eF;if(eB.data.caption){eS.alt=eB.data.caption||""}if(eB.data.full){bV(eS).data("original",eB.data.full)}if(aS.isExpectedCaption(eD,c3.showcaption)){bV(eS).attr("aria-labelledby",eD.labelledby)}})}function cy(){var ex=dF[bu];if(ex&&!ex.data().state){eb.addClass(Z);ex.on("f:load f:error",function(){ex.off("f:load f:error");eb.removeClass(Z)})}}function cL(ex){a(ex,dJ);bL(ex,function(){setTimeout(function(){bU(dO)},0);dT({time:ds,guessIndex:bV(this).data().eq,minMax:dy})})}function dl(ex,ey){dm(ex,ey,function(eB,ez,eG,eD,eA,eC){if(eD){return}eD=eG[eA]=dk[eA].clone();eC=eD.data();eC.data=eG;var eF=eD[0],eE="labelledby"+bV.now();if(ey==="stage"){if(eG.html){bV('<div class="'+bd+'"></div>').append(eG._html?bV(eG.html).removeAttr("id").html(eG._html):eG.html).appendTo(eD)}if(eG.id){eE=eG.id||eE}eG.labelledby=eE;if(aS.isExpectedCaption(eG,c3.showcaption)){bV(bV.Fotorama.jst.frameCaption({caption:eG.caption,labelledby:eE})).appendTo(eD)}eG.video&&eD.addClass(l).append(cH.clone());bL(eF,function(){setTimeout(function(){bU(cf)},0);cm({index:eC.eq,user:true})});c8=c8.add(eD)}else{if(ey==="navDot"){cL(eF);cB=cB.add(eD)}else{if(ey==="navThumb"){cL(eF);eC.$wrap=eD.children(":first");cW=cW.add(eD);if(eG.video){eC.$wrap.append(cH.clone())}}}}})}function cM(ey,ex){return ey&&ey.length&&V(ey,ex)}function di(ex){dm(ex,"stage",function(eB,ez,eE,eD,eA,eC){if(!eD){return}var ey=cZ(ez);eC.eq=ey;er[bu][ey]=eD.css(bV.extend({left:cv?0:a8(ez,en.w,c3.margin,c2)},cv&&b6(0)));if(be(eD[0])){eD.appendTo(cl);cO(eE.$video)}cM(eC.$img,en);cM(eC.$full,en);if(eD.hasClass(x)&&!(eD.attr("aria-hidden")==="false"&&eD.hasClass(a4))){eD.attr("aria-hidden","true")}})}function dp(eB,ex){var ey,ez,eA;if(cq!=="thumbs"||isNaN(eB)){return}ey=-eB;ez=-eB+en.nw;if(c3.navdir==="vertical"){eB=eB-c3.thumbheight;ez=-eB+en.h}cW.each(function(){var eH=bV(this),eD=eH.data(),eC=eD.eq,eG=function(){return{h:ej,w:eD.w}},eF=eG(),eE=c3.navdir==="vertical"?eD.t>ez:eD.l>ez;eF.w=eD.w;if(eD.l+eD.w<ey||eE||cM(eD.$img,eF)){return}ex&&cx([eC],"navThumb",eG)})}function cz(ex,eC,ey){if(!cz[ey]){var eB=ey==="nav"&&c0,eA=0,ez=0;eC.append(ex.filter(function(){var eH,eG=bV(this),eE=eG.data();for(var eF=0,eD=dP.length;eF<eD;eF++){if(eE.data===dP[eF]){eH=true;eE.eq=eF;break}}return eH||eG.remove()&&false}).sort(function(eE,eD){return bV(eE).data().eq-bV(eD).data().eq}).each(function(){var eE=bV(this),eD=eE.data();aS.setThumbAttr(eE,eD.data.caption,"aria-label")}).each(function(){if(!eB){return}var eF=bV(this),eE=eF.data(),eG=Math.round(ej*eE.data.thumbratio)||ch,eD=Math.round(ch/eE.data.thumbratio)||ej;eE.t=ez;eE.h=eD;eE.l=eA;eE.w=eG;eF.css({width:eG});ez+=eD+c3.thumbmargin;eA+=eG+c3.thumbmargin}));cz[ey]=true}}function eh(ex){return ex-d3>en.w/3}function cE(ex){return !dj&&(!(eo+ex)||!(eo-c1+ex))&&!dg}function dh(){var ey=cE(0),ex=cE(1);dW.toggleClass(F,ey).attr(ad(ey,false));da.toggleClass(F,ex).attr(ad(ex,false))}function ev(){var ex=false,ey=false;if(c3.navtype==="thumbs"&&!c3.loop){(eo==0)?ex=true:ex=false;(eo==c3.data.length-1)?ey=true:ey=false}if(c3.navtype==="slides"){var ez=aa(cF,c3.navdir);ez>=dy.max?ex=true:ex=false;ez<=dy.min?ey=true:ey=false}eg.toggleClass(F,ex).attr(ad(ex,true));dX.toggleClass(F,ey).attr(ad(ey,true))}function cJ(){if(cG.ok){cG.prevent={"<":cE(0),">":cE(1)}}}function dI(eD){var eA=eD.data(),eC,eB,ez,ex;if(c0){eC=eA.l;eB=eA.t;ez=eA.w;ex=eA.h}else{eC=eD.position().left;ez=eD.width()}var ey={c:eC+ez/2,min:-eC+c3.thumbmargin*10,max:-eC+en.w-ez-c3.thumbmargin*10};var eE={c:eB+ex/2,min:-eB+c3.thumbmargin*10,max:-eB+en.h-ex-c3.thumbmargin*10};return c3.navdir==="vertical"?eE:ey}function d7(ey){var ex=dF[dr].data();A(c7,{time:ey*1.2,pos:(c3.navdir==="vertical"?ex.t:ex.l),width:ex.w,height:ex.h,direction:c3.navdir})}function dT(eH){var eB=dP[eH.guessIndex][dr],ez=c3.navtype;var eD,ex,eA,eG,eC,ey,eE,eF;if(eB){if(ez==="thumbs"){eD=dy.min!==dy.max;eA=eH.minMax||eD&&dI(dF[dr]);eG=eD&&(eH.keep&&dT.t?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));eC=eD&&(eH.keep&&dT.l?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));ey=(c3.navdir==="vertical"?eG:eC);eE=eD&&bb(ey,dy.min,dy.max)||0;ex=eH.time*1.1;A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir));dT.l=ey}else{eF=aa(cF,c3.navdir);ex=eH.time*1.11;eE=aD(c3,dy,eH.guessIndex,eF,eB,dU,c3.navdir);A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir))}}}function cS(){dN(dr);cd[dr].push(dF[dr].addClass(a4).attr("data-active",true))}function dN(ey){var ex=cd[ey];while(ex.length){ex.shift().removeClass(a4).attr("data-active",false)}}function ce(ey){var ex=er[ey];bV.each(ea,function(eA,ez){delete ex[cZ(ez)]});bV.each(ex,function(ez,eA){delete ex[ez];eA.detach()})}function dC(ey){c2=ed=eo;var ex=dF[bu];if(ex){dN(bu);cd[bu].push(ex.addClass(a4).attr("data-active",true));if(ex.hasClass(x)){ex.attr("aria-hidden","false")}ey||cg.showStage.onEnd(true);a1(cl,0,true);ce(bu);di(ea);d9();c5();a(cl[0],function(){if(!d6.hasClass(M)){cg.requestFullScreen();dM.focus()}})}}function dL(ey,ex){if(!ey){return}bV.each(ex,function(ez,eA){if(!eA){return}bV.extend(eA,{width:ey.width||eA.width,height:ey.height,minwidth:ey.minwidth,maxwidth:ey.maxwidth,minheight:ey.minheight,maxheight:ey.maxheight,ratio:bm(ey.ratio)})})}function dc(ey,ex){d6.trigger(ag+":"+ey,[cg,ex])}function dR(){clearTimeout(cr.t);c6=1;if(c3.stopautoplayontouch){cg.stopAutoplay()}else{cj=true}}function cr(){if(!c6){return}if(!c3.stopautoplayontouch){cw();es()}cr.t=setTimeout(function(){c6=0},ba+b8)}function cw(){cj=!!(dg||el)}function es(){clearTimeout(es.t);bX.stop(es.w);if(!c3.autoplay||cj){if(cg.autoplay){cg.autoplay=false;dc("stopautoplay")}return}if(!cg.autoplay){cg.autoplay=true;dc("startautoplay")}var ey=eo;var ex=dF[bu].data();es.w=bX(function(){return ex.state||ey!==eo},function(){es.t=setTimeout(function(){if(cj||ey!==eo){return}var ez=cK,eA=dP[ez][bu].data();es.w=bX(function(){return eA.state||ez!==cK},function(){if(cj||ez!==cK){return}cg.show(dj?ay(!dH):cK)})},c3.autoplay)})}cg.startAutoplay=function(ex){if(cg.autoplay){return this}cj=el=false;dq(ex||c3.autoplay);es();return this};cg.stopAutoplay=function(){if(cg.autoplay){cj=el=true;es()}return this};cg.showSlide=function(ez){var eA=aa(cF,c3.navdir),eC,eB=500*1.1,ey=c3.navdir==="horizontal"?c3.thumbwidth:c3.thumbheight,ex=function(){ev()};if(ez==="next"){eC=eA-(ey+c3.margin)*b4}if(ez==="prev"){eC=eA+(ey+c3.margin)*b4}eC=a5(eC,dy);dp(eC,true);A(cF,{time:eB,pos:eC,direction:c3.navdir,onEnd:ex})};cg.showWhileLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showNav(ex,eA,eB);return this};cg.showEndLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;return this};function dK(ey){var ex;if(typeof ey!=="object"){ex=ey;ey={}}else{ex=ey.index}ex=ex===">"?ed+1:ex==="<"?ed-1:ex==="<<"?0:ex===">>"?c1-1:ex;ex=isNaN(ex)?aP:ex;ex=typeof ex==="undefined"?eo||0:ex;return ex}function ew(ex){cg.activeIndex=eo=du(ex);d4=dB(eo);cN=ci(eo);cK=cZ(eo+(dH?-1:1));ea=[eo,d4,cN];ed=dj?ex:eo}function cA(ey){var ex=Math.abs(dw-ed),ez=bj(ey.time,function(){return Math.min(ds*(1+(ex-1)/12),ds*2)});if(ey.slow){ez*=10}return ez}cg.showStage=function(ey,eA,eD){cO(dg,dF.i!==dP[cZ(c2)].i);dl(ea,"stage");di(bh?[ed]:[ed,dB(ed),ci(ed)]);cD("go",true);ey||dc("show",{user:eA.user,time:eD});cj=true;var eC=eA.overPos;var ez=cg.showStage.onEnd=function(eE){if(ez.ok){return}ez.ok=true;eE||dC(true);if(!ey){dc("showend",{user:eA.user})}if(!eE&&cu&&cu!==c3.transition){cg.setOptions({transition:cu});cu=false;return}cy();cx(ea,"stage");cD("go",false);cJ();ei();cw();es();if(cg.fullScreen){dF[bu].find("."+Y).attr("aria-hidden",false);dF[bu].find("."+J).attr("aria-hidden",true)}else{dF[bu].find("."+Y).attr("aria-hidden",true);dF[bu].find("."+J).attr("aria-hidden",false)}};if(!cv){A(cl,{pos:-a8(ed,en.w,c3.margin,c2),overPos:eC,time:eD,onEnd:ez})}else{var ex=dF[bu],eB=dP[dw]&&eo!==dw?dP[dw][bu]:null;aq(ex,eB,c8,{time:eD,method:c3.transition,onEnd:ez},cI)}dh()};cg.showNav=function(ey,ez,eA){ev();if(cq){cS();var ex=cn(eo+bb(ed-dw,-1,1));dT({time:eA,coo:ex!==eo&&ez.coo,guessIndex:typeof ez.coo!=="undefined"?ex:eo,keep:ey});if(c0){d7(eA)}}};cg.show=function(eA){cg.longPress.singlePressInProgress=true;var ez=dK(eA);ew(ez);var eB=cA(eA);var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);cg.showNav(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;cg.longPress.singlePressInProgress=false;return this};cg.requestFullScreen=function(){if(c4&&!cg.fullScreen){var ex=bV((cg.activeFrame||{}).$stageFrame||{}).hasClass("fotorama-video-container");if(ex){return}cs=bf.scrollTop();cT=bf.scrollLeft();bU(bf);cD("x",true);dZ=bV.extend({},en);d6.addClass(M).appendTo(I.addClass(bH));R.addClass(bH);cO(dg,true,true);cg.fullScreen=true;if(d2){bB.request(eu)}cg.resize();cx(ea,"stage");cy();dc("fullscreenenter");if(!("ontouchstart" in bo)){dM.focus()}}return this};function cP(){if(cg.fullScreen){cg.fullScreen=false;if(s){bB.cancel(eu)}I.removeClass(bH);R.removeClass(bH);d6.removeClass(M).insertAfter(c9);en=bV.extend({},dZ);cO(dg,true,true);cD("x",false);cg.resize();cx(ea,"stage");bU(bf,cT,cs);dc("fullscreenexit")}}cg.cancelFullScreen=function(){if(d2&&bB.is()){bB.cancel(k)}else{cP()}return this};cg.toggleFullScreen=function(){return cg[(cg.fullScreen?"cancel":"request")+"FullScreen"]()};cg.resize=function(ez){if(!dP){return this}var eC=arguments[1]||0,ey=arguments[2];b4=ck(dk,c3);dL(!cg.fullScreen?bA(ez):{width:bV(bo).width(),maxwidth:null,minwidth:null,height:bV(bo).height(),maxheight:null,minheight:null},[en,ey||cg.fullScreen||c3]);var eB=en.width,ex=en.height,eA=en.ratio,eD=bf.height()-(cq?dO.height():0);if(t(eB)){dk.css({width:""});dk.css({height:""});cf.css({width:""});cf.css({height:""});cl.css({width:""});cl.css({height:""});dO.css({width:""});dO.css({height:""});dk.css({minWidth:en.minwidth||0,maxWidth:en.maxwidth||bK});if(cq==="dots"){dU.hide()}eB=en.W=en.w=dk.width();en.nw=cq&&d(c3.navwidth,eB)||eB;cl.css({width:en.w,marginLeft:(en.W-en.w)/2});ex=d(ex,eD);ex=ex||(eA&&eB/eA);if(ex){eB=Math.round(eB);ex=en.h=Math.round(bb(ex,d(en.minheight,eD),d(en.maxheight,eD)));cf.css({width:eB,height:ex});if(c3.navdir==="vertical"&&!cg.fullscreen){dO.width(c3.thumbwidth+c3.thumbmargin*2)}if(c3.navdir==="horizontal"&&!cg.fullscreen){dO.height(c3.thumbheight+c3.thumbmargin*2)}if(cq==="dots"){dO.width(eB).height("auto");dU.show()}if(c3.navdir==="vertical"&&cg.fullScreen){cf.css("height",bf.height())}if(c3.navdir==="horizontal"&&cg.fullScreen){cf.css("height",bf.height()-dO.height())}if(cq){switch(c3.navdir){case"vertical":dU.removeClass(bZ);dU.removeClass(ax);dU.addClass(b);dO.stop().animate({height:en.h,width:c3.thumbwidth},eC);break;case"list":dU.removeClass(b);dU.removeClass(bZ);dU.addClass(ax);break;default:dU.removeClass(b);dU.removeClass(ax);dU.addClass(bZ);dO.stop().animate({width:en.nw},eC);break}dC();dT({guessIndex:eo,time:eC,keep:true});if(c0&&cz.nav){d7(eC)}}dG=ey||true;eq.ok=true;eq()}}d3=cf.offset().left;i();return this};cg.setOptions=function(ex){bV.extend(c3,ex);dV();return this};cg.shuffle=function(){dP&&aC(dP)&&dV();return this};function co(ex,ey){if(ec){ex.removeClass(S+" "+aL);ex.removeClass(a2+" "+aR);ey&&!dg&&ex.addClass(ey.replace(/^|\s/g," "+bz+"--"))}}cg.longPress={threshold:1,count:0,thumbSlideTime:20,progress:function(){if(!this.inProgress){this.count++;this.inProgress=this.count>this.threshold}},end:function(){if(this.inProgress){this.isEnded=true}},reset:function(){this.count=0;this.inProgress=false;this.isEnded=false}};cg.destroy=function(){cg.cancelFullScreen();cg.stopAutoplay();dP=cg.data=null;dd();ea=[];ce(bu);dV.ok=false;return this};cg.playVideo=function(){var ez=dF,ex=ez.video,ey=eo;if(typeof ex==="object"&&ez.videoReady){d2&&cg.fullScreen&&cg.cancelFullScreen();bX(function(){return !bB.is()||ey!==eo},function(){if(ey===eo){ez.$video=ez.$video||bV(ab(bJ)).append(q(ex));ez.$video.appendTo(ez[bu]);dk.addClass(bt);dg=ez.$video;em();cU.blur();dM.blur();dc("loadvideo")}})}return this};cg.stopVideo=function(){cO(dg,true,true);return this};cg.spliceByIndex=function(ex,ey){ey.i=ex+1;ey.img&&bV.ajax({url:ey.img,type:"HEAD",success:function(){dP.splice(ex,1,ey);dV()}})};function cO(ex,ez,ey){if(ez){dk.removeClass(bt);dg=false;em()}if(ex&&ex!==dg){ex.remove();dc("unloadvideo")}if(ey){cw();es()}}function cp(ex){dk.toggleClass(P,ex)}function ei(ez){if(d5.flow){return}var ex=ez?ez.pageX:ei.x,ey=ex&&!cE(eh(ex))&&c3.click;if(ei.p!==ey&&cf.toggleClass(bC,ey)){ei.p=ey;ei.x=ex}}cf.on("mousemove",ei);function cm(ex){clearTimeout(cm.t);if(c3.clicktransition&&c3.clicktransition!==c3.transition){setTimeout(function(){var ey=c3.transition;cg.setOptions({transition:c3.clicktransition});cu=ey;cm.t=setTimeout(function(){cg.show(ex)},10)},0)}else{cg.show(ex)}}function ct(eA,ey){var ez=eA.target,ex=bV(ez);if(ex.hasClass(T)){cg.playVideo()}else{if(ez===dD){cg.toggleFullScreen()}else{if(dg){ez===d1&&cO(dg,true,true)}else{if(!d6.hasClass(M)){cg.requestFullScreen()}}}}O(eA,true)}function cD(ex,ey){d5[ex]=dy[ex]=ey}d5=ao(cl,{onStart:dR,onMove:function(ey,ex){co(cf,ex.edge)},onTouchEnd:cr,onEnd:function(ex){var ez;co(cf);ez=(aZ&&!dz||ex.touch)&&c3.arrows;if((ex.moved||(ez&&ex.pos!==ex.newPos&&!ex.control))&&ex.$target[0]!==dM[0]){var ey=by(ex.newPos,en.w,c3.margin,c2);cg.show({index:ey,time:cv?ds:ex.time,overPos:ex.overPos,user:true})}else{if(!ex.aborted&&!ex.control){ct(ex.startEvent,ez)}}},timeLow:1,timeHigh:1,friction:2,select:"."+a9+", ."+a9+" *",$wrap:cf,direction:"horizontal"});dy=ao(cF,{onStart:dR,onMove:function(ey,ex){co(dO,ex.edge)},onTouchEnd:cr,onEnd:function(ex){function ey(){dT.l=ex.newPos;cw();es();dp(ex.newPos,true);ev()}if(!ex.moved){var ez=ex.$target.closest("."+aG,cF)[0];ez&&dJ.call(ez,ex.startEvent)}else{if(ex.pos!==ex.newPos){cj=true;A(cF,{time:ex.time,pos:ex.newPos,overPos:ex.overPos,direction:c3.navdir,onEnd:ey});dp(ex.newPos);ec&&co(dO,bp(ex.newPos,dy.min,dy.max,ex.dir))}else{ey()}}},timeLow:0.5,timeHigh:2,friction:5,$wrap:dO,direction:c3.navdir});cG=o(cf,{shift:true,onEnd:function(ey,ex){dR();cr();cg.show({index:ex,slow:ey.altKey})}});ef=o(dO,{onEnd:function(ez,ey){dR();cr();var ex=a1(cF)+ey*0.25;cF.css(b2(bb(ex,dy.min,dy.max),c3.navdir));ec&&co(dO,bp(ex,dy.min,dy.max,c3.navdir));ef.prevent={"<":ex>=dy.max,">":ex<=dy.min};clearTimeout(ef.t);ef.t=setTimeout(function(){dT.l=ex;dp(ex,true)},b8);dp(ex)}});dk.hover(function(){setTimeout(function(){if(c6){return}cp(!(dz=true))},0)},function(){if(!dz){return}cp(!(dz=false))});function dJ(ey){var ex=bV(this).data().eq;if(c3.navtype==="thumbs"){cm({index:ex,slow:ey.altKey,user:true,coo:ey._x-dO.offset().left})}else{cm({index:ex,slow:ey.altKey,user:true})}}function et(ex){cm({index:cU.index(this)?">":"<",slow:ex.altKey,user:true})}z(cU,function(ex){O(ex);et.call(this,ex)},{onStart:function(){dR();d5.control=true},onTouchEnd:cr});z(eg,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show("<")}else{cg.showSlide("prev")}});z(dX,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show(">")}else{cg.showSlide("next")}});function dv(ex){bL(ex,function(){setTimeout(function(){bU(cf)},0);cp(false)})}cU.each(function(){a(this,function(ex){et.call(this,ex)});dv(this)});a(dD,function(){if(d6.hasClass(M)){cg.cancelFullScreen();cl.focus()}else{cg.requestFullScreen();dM.focus()}});dv(dD);function dV(){dn();dQ();if(!dV.i){dV.i=true;var ex=c3.startindex;eo=c2=ed=dw=dx=du(ex)||0}if(c1){if(cV()){return}if(dg){cO(dg,true)}ea=[];ce(bu);dV.ok=true;cg.show({index:eo,time:0});cg.resize()}else{cg.destroy()}}function cV(){if(!cV.f===dH){cV.f=dH;eo=c1-1-eo;cg.reverse();return true}}bV.each("load push pop shift unshift reverse sort splice".split(" "),function(ex,ey){cg[ey]=function(){dP=dP||[];if(ey!=="load"){Array.prototype[ey].apply(dP,arguments)}else{if(arguments[0]&&typeof arguments[0]==="object"&&arguments[0].length){dP=bG(arguments[0])}}dV();return cg}});function eq(){if(eq.ok){eq.ok=false;dc("ready")}}dV()};bV.fn.fotorama=function(i){return this.each(function(){var ce=this,cd=bV(this),cc=cd.data(),cf=cc.fotorama;if(!cf){bX(function(){return !W(ce)},function(){cc.urtext=cd.html();new bV.Fotorama(cd,bV.extend({},Q,bo.fotoramaDefaults,i,cc))})}else{cf.setOptions(i,true)}})};bV.Fotorama.instances=[];function b1(){bV.each(bV.Fotorama.instances,function(cc,i){i.index=cc})}function C(i){bV.Fotorama.instances.push(i);b1()}function av(i){bV.Fotorama.instances.splice(i.index,1);b1()}bV.Fotorama.cache={};bV.Fotorama.measures={};bV=bV||{};bV.Fotorama=bV.Fotorama||{};bV.Fotorama.jst=bV.Fotorama.jst||{};bV.Fotorama.jst.dots=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__nav__frame fotorama__nav__frame--dot" tabindex="0" role="button" data-gallery-role="nav-frame" data-nav-type="thumb" aria-label>\r\n <div class="fotorama__dot"></div>\r\n</div>';return ce};bV.Fotorama.jst.frameCaption=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__caption" aria-hidden="true">\r\n <div class="fotorama__caption__wrap" id="'+((i=(cc.labelledby))==null?"":i)+'">'+((i=(cc.caption))==null?"":i)+"</div>\r\n</div>\r\n";return ce};bV.Fotorama.jst.style=function(cc){var i,ce="",cd=bx.escape;ce+=".fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__nav--thumbs .fotorama__nav__frame{\r\npadding:"+((i=(cc.m))==null?"":i)+"px;\r\nheight:"+((i=(cc.h))==null?"":i)+"px}\r\n.fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__thumb-border{\r\nheight:"+((i=(cc.h))==null?"":i)+"px;\r\nborder-width:"+((i=(cc.b))==null?"":i)+"px;\r\nmargin-top:"+((i=(cc.m))==null?"":i)+"px}";return ce};bV.Fotorama.jst.thumb=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__nav__frame fotorama__nav__frame--thumb" tabindex="0" role="button" data-gallery-role="nav-frame" data-nav-type="thumb" aria-label>\r\n <div class="fotorama__thumb">\r\n </div>\r\n</div>';return ce}})(window,document,location,typeof jQuery!=="undefined"&&jQuery); +fotoramaVersion="4.6.4";(function(bo,k,a3,bV,aP){var ag="fotorama",bH="fotorama__fullscreen",ae=ag+"__wrap",ah=ae+"--css2",aX=ae+"--css3",bt=ae+"--video",ar=ae+"--fade",aw=ae+"--slide",P=ae+"--no-controls",aM=ae+"--no-shadows",U=ae+"--pan-y",a0=ae+"--rtl",az=ae+"--only-active",bN=ae+"--no-captions",f=ae+"--toggle-arrows",a7=ag+"__stage",x=a7+"__frame",l=x+"--video",B=a7+"__shaft",aB=ag+"__grab",bC=ag+"__pointer",aK=ag+"__arr",F=aK+"--disabled",bc=aK+"--prev",r=aK+"--next",bO=ag+"__nav",bq=bO+"-wrap",aH=bO+"__shaft",b=bq+"--vertical",ax=bq+"--list",bZ=bq+"--horizontal",bW=bO+"--dots",ai=bO+"--thumbs",aG=bO+"__frame",br=ag+"__fade",al=br+"-front",n=br+"-rear",aW=ag+"__shadow",bz=aW+"s",S=bz+"--left",aL=bz+"--right",a2=bz+"--top",aR=bz+"--bottom",a4=ag+"__active",a9=ag+"__select",bs=ag+"--hidden",M=ag+"--fullscreen",aJ=ag+"__fullscreen-icon",bP=ag+"__error",bM=ag+"__loading",c=ag+"__loaded",b3=c+"--full",bg=c+"--img",bR=ag+"__grabbing",J=ag+"__img",Y=J+"--full",bS=ag+"__thumb",b0=bS+"__arr--left",H=bS+"__arr--right",cb=bS+"-border",bd=ag+"__html",af=ag+"-video-container",bJ=ag+"__video",T=bJ+"-play",w=bJ+"-close",au=ag+"_horizontal_ratio",aY=ag+"_vertical_ratio",ca=ag+"__spinner",Z=ca+"--show";var E=bV&&bV.fn.jquery.split(".");if(!E||E[0]<1||(E[0]==1&&E[1]<8)){throw"Fotorama requires jQuery 1.8 or later and will not run without it."}var bx={};var ap=(function(co,ct,cj){var cf="2.8.3",cm={},cD=ct.documentElement,cE="modernizr",cB=ct.createElement(cE),cp=cB.style,cg,cw={}.toString,cy=" -webkit- -moz- -o- -ms- ".split(" "),cd="Webkit Moz O ms",cG=cd.split(" "),cq=cd.toLowerCase().split(" "),ck={},ce={},cu={},cA=[],cv=cA.slice,cc,cz=function(cQ,cS,cK,cR){var cJ,cP,cM,cN,cI=ct.createElement("div"),cO=ct.body,cL=cO||ct.createElement("body");if(parseInt(cK,10)){while(cK--){cM=ct.createElement("div");cM.id=cR?cR[cK]:cE+(cK+1);cI.appendChild(cM)}}cJ=["­",'<style id="s',cE,'">',cQ,"</style>"].join("");cI.id=cE;(cO?cI:cL).innerHTML+=cJ;cL.appendChild(cI);if(!cO){cL.style.background="";cL.style.overflow="hidden";cN=cD.style.overflow;cD.style.overflow="hidden";cD.appendChild(cL)}cP=cS(cI,cQ);if(!cO){cL.parentNode.removeChild(cL);cD.style.overflow=cN}else{cI.parentNode.removeChild(cI)}return !!cP},cs=({}).hasOwnProperty,cC;if(!cl(cs,"undefined")&&!cl(cs.call,"undefined")){cC=function(cI,cJ){return cs.call(cI,cJ)}}else{cC=function(cI,cJ){return((cJ in cI)&&cl(cI.constructor.prototype[cJ],"undefined"))}}if(!Function.prototype.bind){Function.prototype.bind=function cH(cK){var cL=this;if(typeof cL!="function"){throw new TypeError()}var cI=cv.call(arguments,1),cJ=function(){if(this instanceof cJ){var cO=function(){};cO.prototype=cL.prototype;var cN=new cO();var cM=cL.apply(cN,cI.concat(cv.call(arguments)));if(Object(cM)===cM){return cM}return cN}else{return cL.apply(cK,cI.concat(cv.call(arguments)))}};return cJ}}function cr(cI){cp.cssText=cI}function ci(cJ,cI){return cr(cy.join(cJ+";")+(cI||""))}function cl(cJ,cI){return typeof cJ===cI}function cn(cJ,cI){return !!~(""+cJ).indexOf(cI)}function cF(cK,cI){for(var cJ in cK){var cL=cK[cJ];if(!cn(cL,"-")&&cp[cL]!==cj){return cI=="pfx"?cL:true}}return false}function cx(cJ,cM,cL){for(var cI in cJ){var cK=cM[cJ[cI]];if(cK!==cj){if(cL===false){return cJ[cI]}if(cl(cK,"function")){return cK.bind(cL||cM)}return cK}}return false}function i(cM,cI,cL){var cJ=cM.charAt(0).toUpperCase()+cM.slice(1),cK=(cM+" "+cG.join(cJ+" ")+cJ).split(" ");if(cl(cI,"string")||cl(cI,"undefined")){return cF(cK,cI)}else{cK=(cM+" "+(cq).join(cJ+" ")+cJ).split(" ");return cx(cK,cI,cL)}}ck.touch=function(){var cI;if(("ontouchstart" in co)||co.DocumentTouch&&ct instanceof DocumentTouch){cI=true}else{cz(["@media (",cy.join("touch-enabled),("),cE,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(cJ){cI=cJ.offsetTop===9})}return cI};ck.csstransforms3d=function(){var cI=!!i("perspective");if(cI&&"webkitPerspective" in cD.style){cz("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(cJ,cK){cI=cJ.offsetLeft===9&&cJ.offsetHeight===3})}return cI};ck.csstransitions=function(){return i("transition")};for(var ch in ck){if(cC(ck,ch)){cc=ch.toLowerCase();cm[cc]=ck[ch]();cA.push((cm[cc]?"":"no-")+cc)}}cm.addTest=function(cJ,cK){if(typeof cJ=="object"){for(var cI in cJ){if(cC(cJ,cI)){cm.addTest(cI,cJ[cI])}}}else{cJ=cJ.toLowerCase();if(cm[cJ]!==cj){return cm}cK=typeof cK=="function"?cK():cK;if(typeof enableClasses!=="undefined"&&enableClasses){cD.className+=" "+(cK?"":"no-")+cJ}cm[cJ]=cK}return cm};cr("");cB=cg=null;cm._version=cf;cm._prefixes=cy;cm._domPrefixes=cq;cm._cssomPrefixes=cG;cm.testProp=function(cI){return cF([cI])};cm.testAllProps=i;cm.testStyles=cz;cm.prefixed=function(cK,cJ,cI){if(!cJ){return i(cK,"pfx")}else{return i(cK,cJ,cI)}};return cm})(bo,k);var bB={ok:false,is:function(){return false},request:function(){},cancel:function(){},event:"",prefix:""},h="webkit moz o ms khtml".split(" ");if(typeof k.cancelFullScreen!="undefined"){bB.ok=true}else{for(var bv=0,N=h.length;bv<N;bv++){bB.prefix=h[bv];if(typeof k[bB.prefix+"CancelFullScreen"]!="undefined"){bB.ok=true;break}}}if(bB.ok){bB.event=bB.prefix+"fullscreenchange";bB.is=function(){switch(this.prefix){case"":return k.fullScreen;case"webkit":return k.webkitIsFullScreen;default:return k[this.prefix+"FullScreen"]}};bB.request=function(i){return(this.prefix==="")?i.requestFullScreen():i[this.prefix+"RequestFullScreen"]()};bB.cancel=function(i){return(this.prefix==="")?k.cancelFullScreen():k[this.prefix+"CancelFullScreen"]()}}function a6(i){var cc="bez_"+bV.makeArray(arguments).join("_").replace(".","p");if(typeof bV.easing[cc]!=="function"){var cd=function(ck,ci){var cf=[null,null],cl=[null,null],cj=[null,null],ch=function(cm,cn){cj[cn]=3*ck[cn];cl[cn]=3*(ci[cn]-ck[cn])-cj[cn];cf[cn]=1-cj[cn]-cl[cn];return cm*(cj[cn]+cm*(cl[cn]+cm*cf[cn]))},cg=function(cm){return cj[0]+cm*(2*cl[0]+3*cf[0]*cm)},ce=function(co){var cm=co,cn=0,cp;while(++cn<14){cp=ch(cm,0)-co;if(Math.abs(cp)<0.001){break}cm-=cp/cg(cm)}return cm};return function(cm){return ch(ce(cm),1)}};bV.easing[cc]=function(cf,cg,ce,ci,ch){return ci*cd([i[0],i[1]],[i[2],i[3]])(cg/ch)+ce}}return cc}var bf=bV(bo),bw=bV(k),R,I,bT=a3.hash.replace("#","")==="quirks",ac=ap.csstransforms3d,aA=ac&&!bT,aN=ac||k.compatMode==="CSS1Compat",s=bB.ok,am=navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i),bh=!aA||am,aZ=navigator.msPointerEnabled,y="onwheel" in k.createElement("div")?"wheel":k.onmousewheel!==aP?"mousewheel":"DOMMouseScroll",b8=250,ba=300,bY=1400,bQ=5000,b9=2,L=64,bk=500,bE=333,bu="$stageFrame",b7="$navDotFrame",bl="$navThumbFrame",bF="auto",u=a6([0.1,0,0.25,1]),bK=1200,b4=1,Q={width:null,minwidth:null,maxwidth:"100%",height:null,minheight:null,maxheight:null,ratio:null,margin:b9,nav:"dots",navposition:"bottom",navwidth:null,thumbwidth:L,thumbheight:L,thumbmargin:b9,thumbborderwidth:b9,allowfullscreen:false,transition:"slide",clicktransition:null,transitionduration:ba,captions:true,startindex:0,loop:false,autoplay:false,stopautoplayontouch:true,keyboard:false,arrows:true,click:true,swipe:false,trackpad:false,shuffle:false,direction:"ltr",shadows:true,showcaption:true,navdir:"horizontal",navarrows:true,navtype:"thumbs"},p={left:true,right:true,down:true,up:true,space:false,home:false,end:false};function g(){}function bb(cd,cc,i){return Math.max(isNaN(cc)?-Infinity:cc,Math.min(isNaN(i)?Infinity:i,cd))}function bi(cc,i){return cc.match(/ma/)&&cc.match(/-?\d+(?!d)/g)[cc.match(/3d/)?(i==="vertical"?13:12):(i==="vertical"?5:4)]}function aa(cc,i){if(aA){return +bi(cc.css("transform"),i)}else{return +cc.css(i==="vertical"?"top":"left").replace("px","")}}function b2(cd,cc){var i={};if(aA){switch(cc){case"vertical":i.transform="translate3d(0, "+(cd)+"px,0)";break;case"list":break;default:i.transform="translate3d("+(cd)+"px,0,0)";break}}else{cc==="vertical"?i.top=cd:i.left=cd}return i}function b6(i){return{"transition-duration":i+"ms"}}function aV(cc,i){return isNaN(cc)?i:cc}function m(cc,i){return aV(+String(cc).replace(i||"px",""))}function K(i){return/%$/.test(i)?m(i,"%"):aP}function d(cc,i){return aV(K(cc)/100*i,m(cc))}function t(i){return(!isNaN(m(i))||!isNaN(m(i,"%")))&&i}function a8(cc,cd,ce,i){return(cc-(i||0))*(cd+(ce||0))}function by(ce,cc,cd,i){return -Math.round(ce/(cc+(cd||0))-(i||0))}function aO(cd){var cc=cd.data();if(cc.tEnd){return}var ce=cd[0],i={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",msTransition:"MSTransitionEnd",transition:"transitionend"};D(ce,i[ap.prefixed("transition")],function(cf){cc.tProp&&cf.propertyName.match(cc.tProp)&&cc.onEndFn()});cc.tEnd=true}function X(cd,cf,ce,cg){var cc,i=cd.data();if(i){i.onEndFn=function(){if(cc){return}cc=true;clearTimeout(i.tT);ce()};i.tProp=cf;clearTimeout(i.tT);i.tT=setTimeout(function(){i.onEndFn()},cg*1.5);aO(cd)}}function a1(cd,cf){var cc=cd.navdir||"horizontal";if(cd.length){var i=cd.data();if(aA){cd.css(b6(0));i.onEndFn=g;clearTimeout(i.tT)}else{cd.stop()}var ce=bj(cf,function(){return aa(cd,cc)});cd.css(b2(ce,cc));return ce}}function bj(){var cc;for(var cd=0,i=arguments.length;cd<i;cd++){cc=cd?arguments[cd]():arguments[cd];if(typeof cc==="number"){break}}return cc}function aF(cc,i){return Math.round(cc+((i-cc)/1.5))}function aU(){aU.p=aU.p||(a3.protocol==="https:"?"https://":"http://");return aU.p}function ak(cc){var i=k.createElement("a");i.href=cc;return i}function at(i,cd){if(typeof i!=="string"){return i}i=ak(i);var cf,ce;if(i.host.match(/youtube\.com/)&&i.search){cf=i.search.split("v=")[1];if(cf){var cc=cf.indexOf("&");if(cc!==-1){cf=cf.substring(0,cc)}ce="youtube"}}else{if(i.host.match(/youtube\.com|youtu\.be/)){cf=i.pathname.replace(/^\/(embed\/|v\/)?/,"").replace(/\/.*/,"");ce="youtube"}else{if(i.host.match(/vimeo\.com/)){ce="vimeo";cf=i.pathname.replace(/^\/(video\/)?/,"").replace(/\/.*/,"")}}}if((!cf||!ce)&&cd){cf=i.href;ce="custom"}return cf?{id:cf,type:ce,s:i.search.replace(/^\?/,""),p:aU()}:false}function aQ(cg,ce,cf){var cc,i,cd=cg.video;if(cd.type==="youtube"){i=aU()+"img.youtube.com/vi/"+cd.id+"/default.jpg";cc=i.replace(/\/default.jpg$/,"/hqdefault.jpg");cg.thumbsReady=true}else{if(cd.type==="vimeo"){bV.ajax({url:aU()+"vimeo.com/api/v2/video/"+cd.id+".json",dataType:"jsonp",success:function(ch){cg.thumbsReady=true;v(ce,{img:ch[0].thumbnail_large,thumb:ch[0].thumbnail_small},cg.i,cf)}})}else{cg.thumbsReady=true}}return{img:cc,thumb:i}}function v(ch,cd,cf,ci){for(var cg=0,ce=ch.length;cg<ce;cg++){var cj=ch[cg];if(cj.i===cf&&cj.thumbsReady){var cc={videoReady:true};cc[bu]=cc[bl]=cc[b7]=false;ci.splice(cg,1,bV.extend({},cj,cc,cd));break}}}function bI(cc){var ce=[];function cd(cf,ch,cn){var ci=cf.children("img").eq(0),cm=cf.attr("href"),ck=cf.attr("src"),cl=ci.attr("src"),cj=ch.video,cg=cn?at(cm,cj===true):false;if(cg){cm=false}else{cg=cj}i(cf,ci,bV.extend(ch,{video:cg,img:ch.img||cm||ck||cl,thumb:ch.thumb||cl||ck||cm}))}function i(cg,cj,ck){var ci=ck.thumb&&ck.img!==ck.thumb,ch=m(ck.width||cg.attr("width")),cf=m(ck.height||cg.attr("height"));bV.extend(ck,{width:ch,height:cf,thumbratio:bm(ck.thumbratio||(m(ck.thumbwidth||(cj&&cj.attr("width"))||ci||ch)/m(ck.thumbheight||(cj&&cj.attr("height"))||ci||cf)))})}cc.children().each(function(){var cf=bV(this),cg=bA(bV.extend(cf.data(),{id:cf.attr("id")}));if(cf.is("a, img")){cd(cf,cg,true)}else{if(!cf.is(":empty")){i(cf,null,bV.extend(cg,{html:this,_html:cf.html()}))}else{return}}ce.push(cg)});return ce}function W(i){return i.offsetWidth===0&&i.offsetHeight===0}function be(i){return !bV.contains(k.documentElement,i)}function bX(cf,cd,ce,cc){if(!bX.i){bX.i=1;bX.ii=[true]}cc=cc||bX.i;if(typeof bX.ii[cc]==="undefined"){bX.ii[cc]=true}if(cf()){cd()}else{bX.ii[cc]&&setTimeout(function(){bX.ii[cc]&&bX(cf,cd,ce,cc)},ce||100)}return bX.i++}bX.stop=function(cc){bX.ii[cc]=false};function V(ce,cd){var cc=ce.data(),cg=cc.measures;if(cg&&(!cc.l||cc.l.W!==cg.width||cc.l.H!==cg.height||cc.l.r!==cg.ratio||cc.l.w!==cd.w||cc.l.h!==cd.h)){var i=bb(cd.h,0,cg.height),cf=i*cg.ratio;aS.setRatio(ce,cf,i);cc.l={W:cg.width,H:cg.height,r:cg.ratio,w:cd.w,h:cd.h}}return true}function an(i,cd){var cc=i[0];if(cc.styleSheet){cc.styleSheet.cssText=cd}else{i.html(cd)}}function bp(ce,cd,i,cc){return cd===i?false:cc==="vertical"?(ce<=cd?"top":ce>=i?"bottom":"top bottom"):(ce<=cd?"left":ce>=i?"right":"left right")}function z(cc,cd,i){i=i||{};cc.each(function(){var cg=bV(this),cf=cg.data(),ce;if(cf.clickOn){return}cf.clickOn=true;bV.extend(aI(cg,{onStart:function(ch){ce=ch;(i.onStart||g).call(this,ch)},onMove:i.onMove||g,onTouchEnd:i.onTouchEnd||g,onEnd:function(ch){if(ch.moved){return}cd.call(this,ce)}}),{noMove:true})})}function ab(i,cc){return'<div class="'+i+'">'+(cc||"")+"</div>"}function aT(i){return"."+i}function q(i){var cc='<iframe src="'+i.p+i.type+".com/embed/"+i.id+'" frameborder="0" allowfullscreen></iframe>';return cc}function aC(cf){var cc=cf.length;while(cc){var ce=Math.floor(Math.random()*cc--);var cd=cf[cc];cf[cc]=cf[ce];cf[ce]=cd}return cf}function bG(i){return Object.prototype.toString.call(i)=="[object Array]"&&bV.map(i,function(cc){return bV.extend({},cc)})}function bU(i,cd,cc){i.scrollLeft(cd||0).scrollTop(cc||0)}function bA(i){if(i){var cc={};bV.each(i,function(cd,ce){cc[cd.toLowerCase()]=ce});return cc}}function bm(i){if(!i){return}var cc=+i;if(!isNaN(cc)){return cc}else{cc=i.split("/");return +cc[0]/+cc[1]||aP}}function D(cd,ce,cc,i){if(!ce){return}cd.addEventListener?cd.addEventListener(ce,cc,!!i):cd.attachEvent("on"+ce,cc)}function a5(i,cc){if(i>cc.max){i=cc.max}else{if(i<cc.min){i=cc.min}}return i}function aD(i,ck,ch,cf,ce,cd,cc){var cg,cj,ci;if(cc==="horizontal"){cj=i.thumbwidth;ci=cd.width()}else{cj=i.thumbheight;ci=cd.height()}if((cj+i.margin)*(ch+1)>=(ci-cf)){if(cc==="horizontal"){cg=-ce.position().left}else{cg=-ce.position().top}}else{if((cj+i.margin)*(ch)<=Math.abs(cf)){if(cc==="horizontal"){cg=-ce.position().left+ci-(cj+i.margin)}else{cg=-ce.position().top+ci-(cj+i.margin)}}else{cg=cf}}cg=a5(cg,ck);return cg||0}function aj(i){return !!i.getAttribute("disabled")}function ad(cc,i){if(i){return{disabled:cc}}else{return{tabindex:cc*-1+"",disabled:cc}}}function a(cc,i){D(cc,"keyup",function(cd){aj(cc)||cd.keyCode==13&&i.call(cc,cd)})}function bL(cc,i){D(cc,"focus",cc.onfocusin=function(cd){i.call(cc,cd)},true)}function O(cc,i){cc.preventDefault?cc.preventDefault():(cc.returnValue=false);i&&cc.stopPropagation&&cc.stopPropagation()}function aE(cd,cc){var i=/iP(ad|hone|od)/i.test(bo.navigator.userAgent);if(i&&cc==="touchend"){cd.on("touchend",function(ce){bw.trigger("mouseup",ce)})}cd.on(cc,function(ce){O(ce,true);return false})}function ay(i){return i?">":"<"}var aS=(function(){function cd(ch,ce,cg){var cf=ce/cg;if(cf<=1){ch.parent().removeClass(au);ch.parent().addClass(aY)}else{ch.parent().removeClass(aY);ch.parent().addClass(au)}}function i(cf,cg,ch){var ce=ch;if(!cf.attr(ce)&&cf.attr(ce)!==aP){cf.attr(ce,cg)}if(cf.find("["+ce+"]").length){cf.find("["+ce+"]").each(function(){bV(this).attr(ce,cg)})}}function cc(cf,ce,ci){var cg=false,ch;cf.showCaption===ci||cf.showCaption===true?ch=true:ch=false;if(!ce){return false}if(cf.caption&&ch){cg=true}return cg}return{setRatio:cd,setThumbAttr:i,isExpectedCaption:cc}}(aS||{},jQuery));function A(ce,cd){var cc=ce.data(),i=Math.round(cd.pos),cf=function(){if(cc&&cc.sliding){cc.sliding=false}(cd.onEnd||g)()};if(typeof cd.overPos!=="undefined"&&cd.overPos!==cd.pos){i=cd.overPos}var cg=bV.extend(b2(i,cd.direction),cd.width&&{width:cd.width},cd.height&&{height:cd.height});if(cc&&cc.sliding){cc.sliding=true}if(aA){ce.css(bV.extend(b6(cd.time),cg));if(cd.time>10){X(ce,"transform",cf,cd.time)}else{cf()}}else{ce.stop().animate(cg,cd.time,u,cf)}}function aq(ck,cj,cc,cm,ce,i){var ch=typeof i!=="undefined";if(!ch){ce.push(arguments);Array.prototype.push.call(arguments,ce.length);if(ce.length>1){return}}ck=ck||bV(ck);cj=cj||bV(cj);var ci=ck[0],cg=cj[0],cf=cm.method==="crossfade",cl=function(){if(!cl.done){cl.done=true;var cn=(ch||ce.shift())&&ce.shift();cn&&aq.apply(this,cn);(cm.onEnd||g)(!!cn)}},cd=cm.time/(i||1);cc.removeClass(n+" "+al);ck.stop().addClass(n);cj.stop().addClass(al);cf&&cg&&ck.fadeTo(0,0);ck.fadeTo(cf?cd:0,1,cf&&cl);cj.fadeTo(cd,0,cl);(ci&&cf)||cg||cl()}var G,b5,e,j,bD;function bn(i){var cc=(i.touches||[])[0]||i;i._x=cc.pageX||cc.originalEvent.pageX;i._y=cc.clientY||cc.originalEvent.clientY;i._now=bV.now()}function aI(cr,cg){var cc=cr[0],cj={},i,cl,cf,cn,cs,cd,ce,co,ch;function cq(ct){cf=bV(ct.target);cj.checked=cd=ce=ch=false;if(i||cj.flow||(ct.touches&&ct.touches.length>1)||ct.which>1||(G&&G.type!==ct.type&&e)||(cd=cg.select&&cf.is(cg.select,cc))){return cd}cs=ct.type==="touchstart";ce=cf.is("a, a *",cc);cn=cj.control;co=(cj.noMove||cj.noSwipe||cn)?16:!cj.snap?4:0;bn(ct);cl=G=ct;b5=ct.type.replace(/down|start/,"move").replace(/Down/,"Move");(cg.onStart||g).call(cc,ct,{control:cn,$target:cf});i=cj.flow=true;if(!cs||cj.go){O(ct)}}function ck(cx){if((cx.touches&&cx.touches.length>1)||(aZ&&!cx.isPrimary)||b5!==cx.type||!i){i&&ci();(cg.onTouchEnd||g)();return}bn(cx);var cy=Math.abs(cx._x-cl._x),cu=Math.abs(cx._y-cl._y),cw=cy-cu,cv=(cj.go||cj.x||cw>=0)&&!cj.noSwipe,ct=cw<0;if(cs&&!cj.checked){if(i=cv){O(cx)}}else{O(cx);(cg.onMove||g).call(cc,cx,{touch:cs})}if(!ch&&Math.sqrt(Math.pow(cy,2)+Math.pow(cu,2))>co){ch=true}cj.checked=cj.checked||cv||ct}function ci(cu){(cg.onTouchEnd||g)();var ct=i;cj.control=i=false;if(ct){cj.flow=false}if(!ct||(ce&&!cj.checked)){return}cu&&O(cu);e=true;clearTimeout(j);j=setTimeout(function(){e=false},1000);(cg.onEnd||g).call(cc,{moved:ch,$target:cf,control:cn,touch:cs,startEvent:cl,aborted:!cu||cu.type==="MSPointerCancel"})}function cm(){if(cj.flow){return}cj.flow=true}function cp(){if(!cj.flow){return}cj.flow=false}if(aZ){D(cc,"MSPointerDown",cq);D(k,"MSPointerMove",ck);D(k,"MSPointerCancel",ci);D(k,"MSPointerUp",ci)}else{D(cc,"touchstart",cq);D(cc,"touchmove",ck);D(cc,"touchend",ci);D(k,"touchstart",cm);D(k,"touchend",cp);D(k,"touchcancel",cp);bf.on("scroll",cp);cr.on("mousedown pointerdown",cq);bw.on("mousemove pointermove",ck).on("mouseup pointerup",ci)}if(ap.touch){bD="a"}else{bD="div"}cr.on("click",bD,function(ct){cj.checked&&O(ct)});return cj}function ao(cz,cd){var cc=cz[0],ce=cz.data(),cm={},cw,cf,cx,cj,ch,cy,co,cg,cr,ct,cp,cq,i,cv,ci,cn;function cs(cA,cB){cn=true;cw=cf=(cq==="vertical")?cA._y:cA._x;co=cA._now;cy=[[co,cw]];cx=cj=cm.noMove||cB?0:a1(cz,(cd.getPos||g)());(cd.onStart||g).call(cc,cA)}function cu(cB,cA){cr=cm.min;ct=cm.max;cp=cm.snap,cq=cm.direction||"horizontal",cz.navdir=cq;i=cB.altKey;cn=ci=false;cv=cA.control;if(!cv&&!ce.sliding){cs(cB)}}function cl(cB,cA){if(!cm.noSwipe){if(!cn){cs(cB)}cf=(cq==="vertical")?cB._y:cB._x;cy.push([cB._now,cf]);cj=cx-(cw-cf);ch=bp(cj,cr,ct,cq);if(cj<=cr){cj=aF(cj,cr)}else{if(cj>=ct){cj=aF(cj,ct)}}if(!cm.noMove){cz.css(b2(cj,cq));if(!ci){ci=true;cA.touch||aZ||cz.addClass(bR)}(cd.onMove||g).call(cc,cB,{pos:cj,edge:ch})}}}function ck(cJ){if(cm.noSwipe&&cJ.moved){return}if(!cn){cs(cJ.startEvent,true)}cJ.touch||aZ||cz.removeClass(bR);cg=bV.now();var cG=cg-b8,cK,cP,cQ,cS=null,cA,cE,cN,cD,cF,cI=ba,cO,cH=cd.friction;for(var cC=cy.length-1;cC>=0;cC--){cK=cy[cC][0];cP=Math.abs(cK-cG);if(cS===null||cP<cQ){cS=cK;cA=cy[cC][1]}else{if(cS===cG||cP>cQ){break}}cQ=cP}cD=bb(cj,cr,ct);var cT=cA-cf,cR=cT>=0,cL=cg-cS,cB=cL>b8,cM=!cB&&cj!==cx&&cD===cj;if(cp){cD=bb(Math[cM?(cR?"floor":"ceil"):"round"](cj/cp)*cp,cr,ct);cr=ct=cD}if(cM&&(cp||cD===cj)){cO=-(cT/cL);cI*=bb(Math.abs(cO),cd.timeLow,cd.timeHigh);cE=Math.round(cj+cO*cI/cH);if(!cp){cD=cE}if(!cR&&cE>ct||cR&&cE<cr){cN=cR?cr:ct;cF=cE-cN;if(!cp){cD=cN}cF=bb(cD+cF*0.03,cN-50,cN+50);cI=Math.abs((cj-cF)/(cO/cH))}}cI*=i?10:1;(cd.onEnd||g).call(cc,bV.extend(cJ,{moved:cJ.moved||cB&&cp,pos:cj,newPos:cD,overPos:cF,time:cI,dir:cq}))}cm=bV.extend(aI(cd.$wrap,bV.extend({},cd,{onStart:cu,onMove:cl,onEnd:ck})),cm);return cm}function o(ce,cd){var cg=ce[0],ch,cf,i,cc={prevent:{}};D(cg,y,function(co){var cl=co.wheelDeltaY||-1*co.deltaY||0,cn=co.wheelDeltaX||-1*co.deltaX||0,ck=Math.abs(cn)&&!Math.abs(cl),cm=ay(cn<0),cp=cf===cm,ci=bV.now(),cj=ci-i<b8;cf=cm;i=ci;if(!ck||!cc.ok||cc.prevent[cm]&&!ch){return}else{O(co,true);if(ch&&cp&&cj){return}}if(cd.shift){ch=true;clearTimeout(cc.t);cc.t=setTimeout(function(){ch=false},bY)}(cd.onEnd||g)(co,cd.shift?cm:cn)});return cc}jQuery.Fotorama=function(d6,c3){R=bV("html");I=bV("body");var cg=this,cC=bV.now(),cQ=ag+cC,eu=d6[0],dP,cY=1,cR=d6.data(),c1,de=bV("<style></style>"),c9=bV(ab(bs)),dk=d6.find(aT(ae)),cf=dk.find(aT(a7)),dY=cf[0],cl=d6.find(aT(B)),c8=bV(),dW=d6.find(aT(bc)),da=d6.find(aT(r)),cU=d6.find(aT(aK)),dU=d6.find(aT(bq)),dO=dU.find(aT(bO)),cF=dO.find(aT(aH)),dA,cB=bV(),cW=bV(),dS=cl.data(),cX=cF.data(),c7=d6.find(aT(cb)),eg=d6.find(aT(b0)),dX=d6.find(aT(H)),dM=d6.find(aT(aJ)),dD=dM[0],cH=bV(ab(T)),dt=d6.find(aT(w)),d1=dt[0],eb=d6.find(aT(ca)),dg,eo=false,dF,ea,c2,ed,dw,d4,cN,cK,dx,dj,cq,c0,d8,c4,d2,cv,ch,ej,ds,cu,ec,dH,dE,d0={},en={},dG,d5={},cG={},dy={},ef={},cs,cT,ee,cj,el,cd={},er={},dZ,c6,dz,dr,d3=0,cI=[];dk[bu]=bV('<div class="'+x+'"></div>');dk[bl]=bV(bV.Fotorama.jst.thumb());dk[b7]=bV(bV.Fotorama.jst.dots());cd[bu]=[];cd[bl]=[];cd[b7]=[];er[bu]={};dk.addClass(aA?aX:ah);cR.fotorama=this;function ep(){bV.each(dP,function(ey,eA){if(!eA.i){eA.i=cY++;var ez=at(eA.video,true);if(ez){var ex={};eA.video=ez;if(!eA.img&&!eA.thumb){ex=aQ(eA,dP,cg)}else{eA.thumbsReady=true}v(dP,{img:ex.img,thumb:ex.thumb},eA.i,cg)}}})}function df(ex){return dE[ex]}function i(){if(cf!==aP){if(c3.navdir=="vertical"){var ex=c3.thumbwidth+c3.thumbmargin;cf.css("left",ex);da.css("right",ex);dM.css("right",ex);dk.css("width",dk.css("width")+ex);cl.css("max-width",dk.width()-ex)}else{cf.css("left","");da.css("right","");dM.css("right","");dk.css("width",dk.css("width")+ex);cl.css("max-width","")}}}function ek(eB){var eC="keydown."+ag,eD=ag+cC,ex="keydown."+eD,eA="keyup."+eD,ey="resize."+eD+" orientationchange."+eD,ez;if(eB){bw.on(ex,function(eG){var eF,eE;if(dg&&eG.keyCode===27){eF=true;cO(dg,true,true)}else{if(cg.fullScreen||(c3.keyboard&&!cg.index)){if(eG.keyCode===27){eF=true;cg.cancelFullScreen()}else{if((eG.shiftKey&&eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===37&&df("left"))||(eG.keyCode===38&&df("up")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE="<"}else{if((eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===39&&df("right"))||(eG.keyCode===40&&df("down")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE=">"}else{if(eG.keyCode===36&&df("home")){cg.longPress.progress();eE="<<"}else{if(eG.keyCode===35&&df("end")){cg.longPress.progress();eE=">>"}}}}}}}(eF||eE)&&O(eG);ez={index:eE,slow:eG.altKey,user:true};eE&&(cg.longPress.inProgress?cg.showWhileLongPress(ez):cg.show(ez))});if(eB){bw.on(eA,function(eE){if(cg.longPress.inProgress){cg.showEndLongPress({user:true})}cg.longPress.reset()})}if(!cg.index){bw.off(eC).on(eC,"textarea, input, select",function(eE){!I.hasClass(bH)&&eE.stopPropagation()})}bf.on(ey,cg.resize)}else{bw.off(ex);bf.off(ey)}}function dd(ex){if(ex===dd.f){return}if(ex){d6.addClass(ag+" "+cQ).before(c9).before(de);C(cg)}else{c9.detach();de.detach();d6.html(cR.urtext).removeClass(cQ);av(cg)}ek(ex);dd.f=ex}function dn(){dP=cg.data=dP||bG(c3.data)||bI(d6);c1=cg.size=dP.length;eq.ok&&c3.shuffle&&aC(dP);ep();eo=cn(eo);c1&&dd(true)}function em(){var ex=c1<2||dg;d5.noMove=ex||cv;d5.noSwipe=ex||!c3.swipe;!cu&&cl.toggleClass(aB,!c3.click&&!d5.noMove&&!d5.noSwipe);aZ&&dk.toggleClass(U,!d5.noSwipe)}function dq(ex){if(ex===true){ex=""}c3.autoplay=Math.max(+ex||bQ,ds*1.5)}function db(ex){if(ex.navarrows&&ex.nav==="thumbs"){eg.show();dX.show()}else{eg.hide();dX.hide()}}function ck(ex,ey){return Math.floor(dk.width()/(ey.thumbwidth+ey.thumbmargin))}function dQ(){if(!c3.nav||c3.nav==="dots"){c3.navdir="horizontal"}cg.options=c3=bA(c3);b4=ck(dk,c3);cv=(c3.transition==="crossfade"||c3.transition==="dissolve");dj=c3.loop&&(c1>2||(cv&&(!cu||cu!=="slide")));ds=+c3.transitionduration||ba;dH=c3.direction==="rtl";dE=bV.extend({},c3.keyboard&&p,c3.keyboard);db(c3);var ey={add:[],remove:[]};function ex(ez,eA){ey[ez?"add":"remove"].push(eA)}if(c1>1){cq=c3.nav;d8=c3.navposition==="top";ey.remove.push(a9);cU.toggle(!!c3.arrows)}else{cq=false;cU.hide()}dh();cJ();ev();if(c3.autoplay){dq(c3.autoplay)}ch=m(c3.thumbwidth)||L;ej=m(c3.thumbheight)||L;cG.ok=ef.ok=c3.trackpad&&!bh;em();dL(c3,[en]);c0=cq==="thumbs";if(dU.filter(":hidden")&&!!cq){dU.show()}if(c0){dl(c1,"navThumb");dA=cW;dr=bl;an(de,bV.Fotorama.jst.style({w:ch,h:ej,b:c3.thumbborderwidth,m:c3.thumbmargin,s:cC,q:!aN}));dO.addClass(ai).removeClass(bW)}else{if(cq==="dots"){dl(c1,"navDot");dA=cB;dr=b7;dO.addClass(bW).removeClass(ai)}else{dU.hide();cq=false;dO.removeClass(ai+" "+bW)}}if(cq){if(d8){dU.insertBefore(cf)}else{dU.insertAfter(cf)}cz.nav=false;cz(dA,cF,"nav")}c4=c3.allowfullscreen;if(c4){dM.prependTo(cf);d2=s&&c4==="native";aE(dM,"touchend")}else{dM.detach();d2=false}ex(cv,ar);ex(!cv,aw);ex(!c3.captions,bN);ex(dH,a0);ex(c3.arrows,f);ec=c3.shadows&&!bh;ex(!ec,aM);dk.addClass(ey.add.join(" ")).removeClass(ey.remove.join(" "));d0=bV.extend({},c3);i()}function cZ(ex){return ex<0?(c1+(ex%c1))%c1:ex>=c1?ex%c1:ex}function cn(ex){return bb(ex,0,c1-1)}function du(ex){return dj?cZ(ex):cn(ex)}function dB(ex){return ex>0||dj?ex-1:false}function ci(ex){return ex<c1-1||dj?ex+1:false}function d9(){d5.min=dj?-Infinity:-a8(c1-1,en.w,c3.margin,c2);d5.max=dj?Infinity:-a8(0,en.w,c3.margin,c2);d5.snap=en.w+c3.margin}function c5(){var ex=(c3.navdir==="vertical");var ez=ex?cF.height():cF.width();var ey=ex?en.h:en.nw;dy.min=Math.min(0,ey-ez);dy.max=0;dy.direction=c3.navdir;cF.toggleClass(aB,!(dy.noMove=dy.min===dy.max))}function dm(ey,eA,ez){if(typeof ey==="number"){ey=new Array(ey);var ex=true}return bV.each(ey,function(eD,eB){if(ex){eB=eD}if(typeof eB==="number"){var eF=dP[cZ(eB)];if(eF){var eC="$"+eA+"Frame",eE=eF[eC];ez.call(this,eD,eB,eF,eE,eC,eE&&eE.data())}}})}function cc(eA,ex,ez,ey){if(!dG||(dG==="*"&&ey===dx)){eA=t(c3.width)||t(eA)||bk;ex=t(c3.height)||t(ex)||bE;cg.resize({width:eA,ratio:c3.ratio||ez||eA/ex},0,ey!==dx&&"*")}}function cx(ex,ey,eA,ez){dm(ex,ey,function(eM,eE,eD,eC,eR,eB){if(!eC){return}var eN=cg.fullScreen&&!eB.$full&&ey==="stage";if(eB.$img&&!ez&&!eN){return}var eS=new Image(),eG=bV(eS),eO=eG.data();eB[eN?"$full":"$img"]=eG;var eJ=ey==="stage"?(eN?"full":"img"):"thumb",eF=eD[eJ],eP=eN?eD.img:eD[ey==="stage"?"thumb":"img"];if(ey==="navThumb"){eC=eB.$wrap}function eH(eT){var eU=cZ(eE);dc(eT,{index:eU,src:eF,frame:dP[eU]})}function eK(){eG.remove();bV.Fotorama.cache[eF]="error";if((!eD.html||ey!=="stage")&&eP&&eP!==eF){eD[eJ]=eF=eP;eB.$full=null;cx([eE],ey,eA,true)}else{if(eF&&!eD.html&&!eN){eC.trigger("f:error").removeClass(bM).addClass(bP);eH("error")}else{if(ey==="stage"){eC.trigger("f:load").removeClass(bM+" "+bP).addClass(c);eH("load");cc()}}eB.state="error";if(c1>1&&dP[eE]===eD&&!eD.html&&!eD.deleted&&!eD.video&&!eN){eD.deleted=true;cg.splice(eE,1)}}}function eL(){bV.Fotorama.measures[eF]=eO.measures=bV.Fotorama.measures[eF]||{width:eS.width,height:eS.height,ratio:eS.width/eS.height};cc(eO.measures.width,eO.measures.height,eO.measures.ratio,eE);eG.off("load error").addClass(""+(eN?Y:J)).attr("aria-hidden","false").prependTo(eC);if(eC.hasClass(x)&&!eC.hasClass(af)){eC.attr("href",eG.attr("src"))}V(eG,(bV.isFunction(eA)?eA():eA)||en);bV.Fotorama.cache[eF]=eB.state="loaded";setTimeout(function(){eC.trigger("f:load").removeClass(bM+" "+bP).addClass(c+" "+(eN?b3:bg));if(ey==="stage"){eH("load")}else{if(eD.thumbratio===bF||!eD.thumbratio&&c3.thumbratio===bF){eD.thumbratio=eO.measures.ratio;dV()}}},0)}if(!eF){eK();return}function eI(){var eT=10;bX(function(){return !c6||!eT--&&!bh},function(){eL()})}if(!bV.Fotorama.cache[eF]){bV.Fotorama.cache[eF]="*";eG.on("load",eI).on("error",eK)}else{(function eQ(){if(bV.Fotorama.cache[eF]==="error"){eK()}else{if(bV.Fotorama.cache[eF]==="loaded"){setTimeout(eI,0)}else{setTimeout(eQ,100)}}})()}eB.state="";eS.src=eF;if(eB.data.caption){eS.alt=eB.data.caption||""}if(eB.data.full){bV(eS).data("original",eB.data.full)}if(aS.isExpectedCaption(eD,c3.showcaption)){bV(eS).attr("aria-labelledby",eD.labelledby)}})}function cy(){var ex=dF[bu];if(ex&&!ex.data().state){eb.addClass(Z);ex.on("f:load f:error",function(){ex.off("f:load f:error");eb.removeClass(Z)})}}function cL(ex){a(ex,dJ);bL(ex,function(){setTimeout(function(){bU(dO)},0);dT({time:ds,guessIndex:bV(this).data().eq,minMax:dy})})}function dl(ex,ey){dm(ex,ey,function(eB,ez,eG,eD,eA,eC){if(eD){return}eD=eG[eA]=dk[eA].clone();eC=eD.data();eC.data=eG;var eF=eD[0],eE="labelledby"+bV.now();if(ey==="stage"){if(eG.html){bV('<div class="'+bd+'"></div>').append(eG._html?bV(eG.html).removeAttr("id").html(eG._html):eG.html).appendTo(eD)}if(eG.id){eE=eG.id||eE}eG.labelledby=eE;if(aS.isExpectedCaption(eG,c3.showcaption)){bV(bV.Fotorama.jst.frameCaption({caption:eG.caption,labelledby:eE})).appendTo(eD)}eG.video&&eD.addClass(l).append(cH.clone());bL(eF,function(){setTimeout(function(){bU(cf)},0);cm({index:eC.eq,user:true})});c8=c8.add(eD)}else{if(ey==="navDot"){cL(eF);cB=cB.add(eD)}else{if(ey==="navThumb"){cL(eF);eC.$wrap=eD.children(":first");cW=cW.add(eD);if(eG.video){eC.$wrap.append(cH.clone())}}}}})}function cM(ey,ex){return ey&&ey.length&&V(ey,ex)}function di(ex){dm(ex,"stage",function(eB,ez,eE,eD,eA,eC){if(!eD){return}var ey=cZ(ez);eC.eq=ey;er[bu][ey]=eD.css(bV.extend({left:cv?0:a8(ez,en.w,c3.margin,c2)},cv&&b6(0)));if(be(eD[0])){eD.appendTo(cl);cO(eE.$video)}cM(eC.$img,en);cM(eC.$full,en);if(eD.hasClass(x)&&!(eD.attr("aria-hidden")==="false"&&eD.hasClass(a4))){eD.attr("aria-hidden","true")}})}function dp(eB,ex){var ey,ez,eA;if(cq!=="thumbs"||isNaN(eB)){return}ey=-eB;ez=-eB+en.nw;if(c3.navdir==="vertical"){eB=eB-c3.thumbheight;ez=-eB+en.h}cW.each(function(){var eH=bV(this),eD=eH.data(),eC=eD.eq,eG=function(){return{h:ej,w:eD.w}},eF=eG(),eE=c3.navdir==="vertical"?eD.t>ez:eD.l>ez;eF.w=eD.w;if(eD.l+eD.w<ey||eE||cM(eD.$img,eF)){return}ex&&cx([eC],"navThumb",eG)})}function cz(ex,eC,ey){if(!cz[ey]){var eB=ey==="nav"&&c0,eA=0,ez=0;eC.append(ex.filter(function(){var eH,eG=bV(this),eE=eG.data();for(var eF=0,eD=dP.length;eF<eD;eF++){if(eE.data===dP[eF]){eH=true;eE.eq=eF;break}}return eH||eG.remove()&&false}).sort(function(eE,eD){return bV(eE).data().eq-bV(eD).data().eq}).each(function(){var eE=bV(this),eD=eE.data();aS.setThumbAttr(eE,eD.data.caption,"aria-label")}).each(function(){if(!eB){return}var eF=bV(this),eE=eF.data(),eG=Math.round(ej*eE.data.thumbratio)||ch,eD=Math.round(ch/eE.data.thumbratio)||ej;eE.t=ez;eE.h=eD;eE.l=eA;eE.w=eG;eF.css({width:eG});ez+=eD+c3.thumbmargin;eA+=eG+c3.thumbmargin}));cz[ey]=true}}function eh(ex){return ex-d3>en.w/3}function cE(ex){return !dj&&(!(eo+ex)||!(eo-c1+ex))&&!dg}function dh(){var ey=cE(0),ex=cE(1);dW.toggleClass(F,ey).attr(ad(ey,false));da.toggleClass(F,ex).attr(ad(ex,false))}function ev(){var ex=false,ey=false;if(c3.navtype==="thumbs"&&!c3.loop){(eo==0)?ex=true:ex=false;(eo==c3.data.length-1)?ey=true:ey=false}if(c3.navtype==="slides"){var ez=aa(cF,c3.navdir);ez>=dy.max?ex=true:ex=false;ez<=dy.min?ey=true:ey=false}eg.toggleClass(F,ex).attr(ad(ex,true));dX.toggleClass(F,ey).attr(ad(ey,true))}function cJ(){if(cG.ok){cG.prevent={"<":cE(0),">":cE(1)}}}function dI(eD){var eA=eD.data(),eC,eB,ez,ex;if(c0){eC=eA.l;eB=eA.t;ez=eA.w;ex=eA.h}else{eC=eD.position().left;ez=eD.width()}var ey={c:eC+ez/2,min:-eC+c3.thumbmargin*10,max:-eC+en.w-ez-c3.thumbmargin*10};var eE={c:eB+ex/2,min:-eB+c3.thumbmargin*10,max:-eB+en.h-ex-c3.thumbmargin*10};return c3.navdir==="vertical"?eE:ey}function d7(ey){var ex=dF[dr].data();A(c7,{time:ey*1.2,pos:(c3.navdir==="vertical"?ex.t:ex.l),width:ex.w,height:ex.h,direction:c3.navdir})}function dT(eH){var eB=dP[eH.guessIndex][dr],ez=c3.navtype;var eD,ex,eA,eG,eC,ey,eE,eF;if(eB){if(ez==="thumbs"){eD=dy.min!==dy.max;eA=eH.minMax||eD&&dI(dF[dr]);eG=eD&&(eH.keep&&dT.t?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));eC=eD&&(eH.keep&&dT.l?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));ey=(c3.navdir==="vertical"?eG:eC);eE=eD&&bb(ey,dy.min,dy.max)||0;ex=eH.time*1.1;A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir));dT.l=ey}else{eF=aa(cF,c3.navdir);ex=eH.time*1.11;eE=aD(c3,dy,eH.guessIndex,eF,eB,dU,c3.navdir);A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir))}}}function cS(){dN(dr);cd[dr].push(dF[dr].addClass(a4).attr("data-active",true))}function dN(ey){var ex=cd[ey];while(ex.length){ex.shift().removeClass(a4).attr("data-active",false)}}function ce(ey){var ex=er[ey];bV.each(ea,function(eA,ez){delete ex[cZ(ez)]});bV.each(ex,function(ez,eA){delete ex[ez];eA.detach()})}function dC(ey){c2=ed=eo;var ex=dF[bu];if(ex){dN(bu);cd[bu].push(ex.addClass(a4).attr("data-active",true));if(ex.hasClass(x)){ex.attr("aria-hidden","false")}ey||cg.showStage.onEnd(true);a1(cl,0,true);ce(bu);di(ea);d9();c5();a(cl[0],function(){if(!d6.hasClass(M)){cg.requestFullScreen();dM.focus()}})}}function dL(ey,ex){if(!ey){return}bV.each(ex,function(ez,eA){if(!eA){return}bV.extend(eA,{width:ey.width||eA.width,height:ey.height,minwidth:ey.minwidth,maxwidth:ey.maxwidth,minheight:ey.minheight,maxheight:ey.maxheight,ratio:bm(ey.ratio)})})}function dc(ey,ex){d6.trigger(ag+":"+ey,[cg,ex])}function dR(){clearTimeout(cr.t);c6=1;if(c3.stopautoplayontouch){cg.stopAutoplay()}else{cj=true}}function cr(){if(!c6){return}if(!c3.stopautoplayontouch){cw();es()}cr.t=setTimeout(function(){c6=0},ba+b8)}function cw(){cj=!!(dg||el)}function es(){clearTimeout(es.t);bX.stop(es.w);if(!c3.autoplay||cj){if(cg.autoplay){cg.autoplay=false;dc("stopautoplay")}return}if(!cg.autoplay){cg.autoplay=true;dc("startautoplay")}var ey=eo;var ex=dF[bu].data();es.w=bX(function(){return ex.state||ey!==eo},function(){es.t=setTimeout(function(){if(cj||ey!==eo){return}var ez=cK,eA=dP[ez][bu].data();es.w=bX(function(){return eA.state||ez!==cK},function(){if(cj||ez!==cK){return}cg.show(dj?ay(!dH):cK)})},c3.autoplay)})}cg.startAutoplay=function(ex){if(cg.autoplay){return this}cj=el=false;dq(ex||c3.autoplay);es();return this};cg.stopAutoplay=function(){if(cg.autoplay){cj=el=true;es()}return this};cg.showSlide=function(ez){var eA=aa(cF,c3.navdir),eC,eB=500*1.1,ey=c3.navdir==="horizontal"?c3.thumbwidth:c3.thumbheight,ex=function(){ev()};if(ez==="next"){eC=eA-(ey+c3.margin)*b4}if(ez==="prev"){eC=eA+(ey+c3.margin)*b4}eC=a5(eC,dy);dp(eC,true);A(cF,{time:eB,pos:eC,direction:c3.navdir,onEnd:ex})};cg.showWhileLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showNav(ex,eA,eB);return this};cg.showEndLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;return this};function dK(ey){var ex;if(typeof ey!=="object"){ex=ey;ey={}}else{ex=ey.index}ex=ex===">"?ed+1:ex==="<"?ed-1:ex==="<<"?0:ex===">>"?c1-1:ex;ex=isNaN(ex)?aP:ex;ex=typeof ex==="undefined"?eo||0:ex;return ex}function ew(ex){cg.activeIndex=eo=du(ex);d4=dB(eo);cN=ci(eo);cK=cZ(eo+(dH?-1:1));ea=[eo,d4,cN];ed=dj?ex:eo}function cA(ey){var ex=Math.abs(dw-ed),ez=bj(ey.time,function(){return Math.min(ds*(1+(ex-1)/12),ds*2)});if(ey.slow){ez*=10}return ez}cg.showStage=function(ey,eA,eD){cO(dg,dF.i!==dP[cZ(c2)].i);dl(ea,"stage");di(bh?[ed]:[ed,dB(ed),ci(ed)]);cD("go",true);ey||dc("show",{user:eA.user,time:eD});cj=true;var eC=eA.overPos;var ez=cg.showStage.onEnd=function(eE){if(ez.ok){return}ez.ok=true;eE||dC(true);if(!ey){dc("showend",{user:eA.user})}if(!eE&&cu&&cu!==c3.transition){cg.setOptions({transition:cu});cu=false;return}cy();cx(ea,"stage");cD("go",false);cJ();ei();cw();es();if(cg.fullScreen){dF[bu].find("."+Y).attr("aria-hidden",false);dF[bu].find("."+J).attr("aria-hidden",true)}else{dF[bu].find("."+Y).attr("aria-hidden",true);dF[bu].find("."+J).attr("aria-hidden",false)}};if(!cv){A(cl,{pos:-a8(ed,en.w,c3.margin,c2),overPos:eC,time:eD,onEnd:ez})}else{var ex=dF[bu],eB=dP[dw]&&eo!==dw?dP[dw][bu]:null;aq(ex,eB,c8,{time:eD,method:c3.transition,onEnd:ez},cI)}dh()};cg.showNav=function(ey,ez,eA){ev();if(cq){cS();var ex=cn(eo+bb(ed-dw,-1,1));dT({time:eA,coo:ex!==eo&&ez.coo,guessIndex:typeof ez.coo!=="undefined"?ex:eo,keep:ey});if(c0){d7(eA)}}};cg.show=function(eA){cg.longPress.singlePressInProgress=true;var ez=dK(eA);ew(ez);var eB=cA(eA);var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);cg.showNav(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;cg.longPress.singlePressInProgress=false;return this};cg.requestFullScreen=function(){if(c4&&!cg.fullScreen){var ex=bV((cg.activeFrame||{}).$stageFrame||{}).hasClass("fotorama-video-container");if(ex){return}cs=bf.scrollTop();cT=bf.scrollLeft();bU(bf);cD("x",true);dZ=bV.extend({},en);d6.addClass(M).appendTo(I.addClass(bH));R.addClass(bH);cO(dg,true,true);cg.fullScreen=true;if(d2){bB.request(eu)}cg.resize();cx(ea,"stage");cy();dc("fullscreenenter");if(!("ontouchstart" in bo)){dM.focus()}}return this};function cP(){if(cg.fullScreen){cg.fullScreen=false;if(s){bB.cancel(eu)}I.removeClass(bH);R.removeClass(bH);d6.removeClass(M).insertAfter(c9);en=bV.extend({},dZ);cO(dg,true,true);cD("x",false);cg.resize();cx(ea,"stage");bU(bf,cT,cs);dc("fullscreenexit")}}cg.cancelFullScreen=function(){if(d2&&bB.is()){bB.cancel(k)}else{cP()}return this};cg.toggleFullScreen=function(){return cg[(cg.fullScreen?"cancel":"request")+"FullScreen"]()};cg.resize=function(ez){if(!dP){return this}var eC=arguments[1]||0,ey=arguments[2];b4=ck(dk,c3);dL(!cg.fullScreen?bA(ez):{width:bV(bo).width(),maxwidth:null,minwidth:null,height:bV(bo).height(),maxheight:null,minheight:null},[en,ey||cg.fullScreen||c3]);var eB=en.width,ex=en.height,eA=en.ratio,eD=bf.height()-(cq?dO.height():0);if(t(eB)){dk.css({width:""});dk.css({height:""});cf.css({width:""});cf.css({height:""});cl.css({width:""});cl.css({height:""});dO.css({width:""});dO.css({height:""});dk.css({minWidth:en.minwidth||0,maxWidth:en.maxwidth||bK});if(cq==="dots"){dU.hide()}eB=en.W=en.w=dk.width();en.nw=cq&&d(c3.navwidth,eB)||eB;cl.css({width:en.w,marginLeft:(en.W-en.w)/2});ex=d(ex,eD);ex=ex||(eA&&eB/eA);if(ex){eB=Math.round(eB);ex=en.h=Math.round(bb(ex,d(en.minheight,eD),d(en.maxheight,eD)));cf.css({width:eB,height:ex});if(c3.navdir==="vertical"&&!cg.fullscreen){dO.width(c3.thumbwidth+c3.thumbmargin*2)}if(c3.navdir==="horizontal"&&!cg.fullscreen){dO.height(c3.thumbheight+c3.thumbmargin*2)}if(cq==="dots"){dO.width(eB).height("auto");dU.show()}if(c3.navdir==="vertical"&&cg.fullScreen){cf.css("height",bf.height())}if(c3.navdir==="horizontal"&&cg.fullScreen){cf.css("height",bf.height()-dO.height())}if(cq){switch(c3.navdir){case"vertical":dU.removeClass(bZ);dU.removeClass(ax);dU.addClass(b);dO.stop().animate({height:en.h,width:c3.thumbwidth},eC);break;case"list":dU.removeClass(b);dU.removeClass(bZ);dU.addClass(ax);break;default:dU.removeClass(b);dU.removeClass(ax);dU.addClass(bZ);dO.stop().animate({width:en.nw},eC);break}dC();dT({guessIndex:eo,time:eC,keep:true});if(c0&&cz.nav){d7(eC)}}dG=ey||true;eq.ok=true;eq()}}d3=cf.offset().left;i();return this};cg.setOptions=function(ex){bV.extend(c3,ex);dV();return this};cg.shuffle=function(){dP&&aC(dP)&&dV();return this};function co(ex,ey){if(ec){ex.removeClass(S+" "+aL);ex.removeClass(a2+" "+aR);ey&&!dg&&ex.addClass(ey.replace(/^|\s/g," "+bz+"--"))}}cg.longPress={threshold:1,count:0,thumbSlideTime:20,progress:function(){if(!this.inProgress){this.count++;this.inProgress=this.count>this.threshold}},end:function(){if(this.inProgress){this.isEnded=true}},reset:function(){this.count=0;this.inProgress=false;this.isEnded=false}};cg.destroy=function(){cg.cancelFullScreen();cg.stopAutoplay();dP=cg.data=null;dd();ea=[];ce(bu);dV.ok=false;return this};cg.playVideo=function(){var ez=dF,ex=ez.video,ey=eo;if(typeof ex==="object"&&ez.videoReady){d2&&cg.fullScreen&&cg.cancelFullScreen();bX(function(){return !bB.is()||ey!==eo},function(){if(ey===eo){ez.$video=ez.$video||bV(ab(bJ)).append(q(ex));ez.$video.appendTo(ez[bu]);dk.addClass(bt);dg=ez.$video;em();cU.blur();dM.blur();dc("loadvideo")}})}return this};cg.stopVideo=function(){cO(dg,true,true);return this};cg.spliceByIndex=function(ex,ey){ey.i=ex+1;ey.img&&bV.ajax({url:ey.img,type:"HEAD",success:function(){dP.splice(ex,1,ey);dV()}})};function cO(ex,ez,ey){if(ez){dk.removeClass(bt);dg=false;em()}if(ex&&ex!==dg){ex.remove();dc("unloadvideo")}if(ey){cw();es()}}function cp(ex){dk.toggleClass(P,ex)}function ei(ez){if(d5.flow){return}var ex=ez?ez.pageX:ei.x,ey=ex&&!cE(eh(ex))&&c3.click;if(ei.p!==ey&&cf.toggleClass(bC,ey)){ei.p=ey;ei.x=ex}}cf.on("mousemove",ei);function cm(ex){clearTimeout(cm.t);if(c3.clicktransition&&c3.clicktransition!==c3.transition){setTimeout(function(){var ey=c3.transition;cg.setOptions({transition:c3.clicktransition});cu=ey;cm.t=setTimeout(function(){cg.show(ex)},10)},0)}else{cg.show(ex)}}function ct(eA,ey){var ez=eA.target,ex=bV(ez);if(ex.hasClass(T)){cg.playVideo()}else{if(ez===dD){cg.toggleFullScreen()}else{if(dg){ez===d1&&cO(dg,true,true)}else{if(!d6.hasClass(M)){cg.requestFullScreen()}}}}O(eA,true)}function cD(ex,ey){d5[ex]=dy[ex]=ey}d5=ao(cl,{onStart:dR,onMove:function(ey,ex){co(cf,ex.edge)},onTouchEnd:cr,onEnd:function(ex){var ez;co(cf);ez=(aZ&&!dz||ex.touch)&&c3.arrows;if((ex.moved||(ez&&ex.pos!==ex.newPos&&!ex.control))&&ex.$target[0]!==dM[0]){var ey=by(ex.newPos,en.w,c3.margin,c2);cg.show({index:ey,time:cv?ds:ex.time,overPos:ex.overPos,user:true})}else{if(!ex.aborted&&!ex.control){ct(ex.startEvent,ez)}}},timeLow:1,timeHigh:1,friction:2,select:"."+a9+", ."+a9+" *",$wrap:cf,direction:"horizontal"});dy=ao(cF,{onStart:dR,onMove:function(ey,ex){co(dO,ex.edge)},onTouchEnd:cr,onEnd:function(ex){function ey(){dT.l=ex.newPos;cw();es();dp(ex.newPos,true);ev()}if(!ex.moved){var ez=ex.$target.closest("."+aG,cF)[0];ez&&dJ.call(ez,ex.startEvent)}else{if(ex.pos!==ex.newPos){cj=true;A(cF,{time:ex.time,pos:ex.newPos,overPos:ex.overPos,direction:c3.navdir,onEnd:ey});dp(ex.newPos);ec&&co(dO,bp(ex.newPos,dy.min,dy.max,ex.dir))}else{ey()}}},timeLow:0.5,timeHigh:2,friction:5,$wrap:dO,direction:c3.navdir});cG=o(cf,{shift:true,onEnd:function(ey,ex){dR();cr();cg.show({index:ex,slow:ey.altKey})}});ef=o(dO,{onEnd:function(ez,ey){dR();cr();var ex=a1(cF)+ey*0.25;cF.css(b2(bb(ex,dy.min,dy.max),c3.navdir));ec&&co(dO,bp(ex,dy.min,dy.max,c3.navdir));ef.prevent={"<":ex>=dy.max,">":ex<=dy.min};clearTimeout(ef.t);ef.t=setTimeout(function(){dT.l=ex;dp(ex,true)},b8);dp(ex)}});dk.hover(function(){setTimeout(function(){if(c6){return}cp(!(dz=true))},0)},function(){if(!dz){return}cp(!(dz=false))});function dJ(ey){var ex=bV(this).data().eq;if(c3.navtype==="thumbs"){cm({index:ex,slow:ey.altKey,user:true,coo:ey._x-dO.offset().left})}else{cm({index:ex,slow:ey.altKey,user:true})}}function et(ex){cm({index:cU.index(this)?">":"<",slow:ex.altKey,user:true})}z(cU,function(ex){O(ex);et.call(this,ex)},{onStart:function(){dR();d5.control=true},onTouchEnd:cr});z(eg,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show("<")}else{cg.showSlide("prev")}});z(dX,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show(">")}else{cg.showSlide("next")}});function dv(ex){bL(ex,function(){setTimeout(function(){bU(cf)},0);cp(false)})}cU.each(function(){a(this,function(ex){et.call(this,ex)});dv(this)});a(dD,function(){if(d6.hasClass(M)){cg.cancelFullScreen();cl.focus()}else{cg.requestFullScreen();dM.focus()}});dv(dD);function dV(){dn();dQ();if(!dV.i){dV.i=true;var ex=c3.startindex;eo=c2=ed=dw=dx=du(ex)||0}if(c1){if(cV()){return}if(dg){cO(dg,true)}ea=[];ce(bu);dV.ok=true;cg.show({index:eo,time:0});cg.resize()}else{cg.destroy()}}function cV(){if(!cV.f===dH){cV.f=dH;eo=c1-1-eo;cg.reverse();return true}}bV.each("load push pop shift unshift reverse sort splice".split(" "),function(ex,ey){cg[ey]=function(){dP=dP||[];if(ey!=="load"){Array.prototype[ey].apply(dP,arguments)}else{if(arguments[0]&&typeof arguments[0]==="object"&&arguments[0].length){dP=bG(arguments[0])}}dV();return cg}});function eq(){if(eq.ok){eq.ok=false;dc("ready")}}dV()};bV.fn.fotorama=function(i){return this.each(function(){var ce=this,cd=bV(this),cc=cd.data(),cf=cc.fotorama;if(!cf){bX(function(){return !W(ce)},function(){cc.urtext=cd.html();new bV.Fotorama(cd,bV.extend({},Q,bo.fotoramaDefaults,i,cc))})}else{cf.setOptions(i,true)}})};bV.Fotorama.instances=[];function b1(){bV.each(bV.Fotorama.instances,function(cc,i){i.index=cc})}function C(i){bV.Fotorama.instances.push(i);b1()}function av(i){bV.Fotorama.instances.splice(i.index,1);b1()}bV.Fotorama.cache={};bV.Fotorama.measures={};bV=bV||{};bV.Fotorama=bV.Fotorama||{};bV.Fotorama.jst=bV.Fotorama.jst||{};bV.Fotorama.jst.dots=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__nav__frame fotorama__nav__frame--dot" tabindex="0" role="button" data-gallery-role="nav-frame" data-nav-type="thumb" aria-label>\r\n <div class="fotorama__dot"></div>\r\n</div>';return ce};bV.Fotorama.jst.frameCaption=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__caption" aria-hidden="true">\r\n <div class="fotorama__caption__wrap" id="'+((i=(cc.labelledby))==null?"":i)+'">'+((i=(cc.caption))==null?"":i)+"</div>\r\n</div>\r\n";return ce};bV.Fotorama.jst.style=function(cc){var i,ce="",cd=bx.escape;ce+=".fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__nav--thumbs .fotorama__nav__frame{\r\npadding:"+((i=(cc.m))==null?"":i)+"px;\r\nheight:"+((i=(cc.h))==null?"":i)+"px}\r\n.fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__thumb-border{\r\nheight:"+((i=(cc.h))==null?"":i)+"px;\r\nborder-width:"+((i=(cc.b))==null?"":i)+"px;\r\nmargin-top:"+((i=(cc.m))==null?"":i)+"px}";return ce};bV.Fotorama.jst.thumb=function(cc){var i,ce="",cd=bx.escape;ce+='<div class="fotorama__nav__frame fotorama__nav__frame--thumb" tabindex="0" role="button" data-gallery-role="nav-frame" data-nav-type="thumb" aria-label>\r\n <div class="fotorama__thumb">\r\n </div>\r\n</div>';return ce}})(window,document,location,typeof jQuery!=="undefined"&&jQuery); diff --git a/lib/web/i18n/en_US.csv b/lib/web/i18n/en_US.csv index 4acc62aa6dc81..7938068871963 100644 --- a/lib/web/i18n/en_US.csv +++ b/lib/web/i18n/en_US.csv @@ -27,6 +27,7 @@ Submit,Submit "Letters, numbers, spaces or underscores only please","Letters, numbers, spaces or underscores only please" "Letters only please","Letters only please" "No white space please","No white space please" +"No marginal white space please","No marginal white space please" "Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx","Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx" "A positive or negative non-decimal number please","A positive or negative non-decimal number please" "The specified vehicle identification number (VIN) is invalid.","The specified vehicle identification number (VIN) is invalid." diff --git a/lib/web/mage/collapsible.js b/lib/web/mage/collapsible.js index 656afc8f89e25..2262c1f74fbe7 100644 --- a/lib/web/mage/collapsible.js +++ b/lib/web/mage/collapsible.js @@ -444,14 +444,19 @@ define([ * Activate. */ activate: function () { - if (!this.options.disabled) { - if (this.options.animate) { - this._animate(showProps); - } else { - this.content.show(); + if (this.options.disabled) { + return; + } + + if (this.options.animate) { + this._animate(showProps); + } else { + if (this.content.length) { + this._scrollToTopIfVisible(this.content.get(0).parentElement); } - this._open(); + this.content.show(); } + this._open(); }, /** @@ -553,6 +558,32 @@ define([ }, 1); }); } + }, + + /** + * @param {HTMLElement} elem + * @private + */ + _scrollToTopIfVisible: function (elem) { + if (this._isElementOutOfViewport(elem)) { + elem.scrollIntoView(); + } + }, + + /** + * @param {HTMLElement} elem + * @private + * @return {Boolean} + */ + _isElementOutOfViewport: function (elem) { + var rect = elem.getBoundingClientRect(); + + return ( + rect.left < 0 || + rect.top < 0 || + rect.right > window.innerWidth || + rect.bottom > window.innerHeight + ); } }); diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 5f088f0446bb5..e14ecf982bc96 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -253,6 +253,12 @@ }, $.mage.__('No white space please') ], + 'no-marginal-whitespace': [ + function (value, element) { + return this.optional(element) || !/^\s+|\s+$/i.test(value); + }, + $.mage.__('No marginal white space please') + ], 'zip-range': [ function (value, element) { return this.optional(element) || /^90[2-5]-\d{2}-\d{4}$/.test(value); diff --git a/pub/errors/processor.php b/pub/errors/processor.php index 9d010ee983436..88e9d2bf5294c 100644 --- a/pub/errors/processor.php +++ b/pub/errors/processor.php @@ -268,10 +268,11 @@ public function getHostUrl() $isSecure = (!empty($_SERVER['HTTPS'])) && ($_SERVER['HTTPS'] != 'off'); $url = ($isSecure ? 'https://' : 'http://') . $host; - if (!empty($_SERVER['SERVER_PORT']) && !in_array($_SERVER['SERVER_PORT'], [80, 443]) + $port = explode(':', $host); + if (isset($port[1]) && !in_array($port[1], [80, 443]) && !preg_match('/.*?\:[0-9]+$/', $url) ) { - $url .= ':' . $_SERVER['SERVER_PORT']; + $url .= ':' . $port[1]; } return $url; } @@ -379,6 +380,8 @@ protected function _loadXml($xmlFile) } /** + * Render page + * * @param string $template * @return string */ diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 9d07cf3193266..80abe6da87f3c 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -1360,7 +1360,7 @@ adminCategoryIdsList.add(vars.get("category_id"));</stringProp> <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regular Expression Extractor: Extract product ids" enabled="true"> <stringProp name="RegexExtractor.useHeaders">false</stringProp> <stringProp name="RegexExtractor.refname">configurable_product_ids</stringProp> - <stringProp name="RegexExtractor.regex">\"id\":(\d+),</stringProp> + <stringProp name="RegexExtractor.regex">\"id\":(\d+),\"sku\"</stringProp> <stringProp name="RegexExtractor.template">$1$</stringProp> <stringProp name="RegexExtractor.default"/> <stringProp name="RegexExtractor.match_number">-1</stringProp> @@ -1570,7 +1570,7 @@ productList.add(productMap);</stringProp> <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regular Expression Extractor: Extract product ids" enabled="true"> <stringProp name="RegexExtractor.useHeaders">false</stringProp> <stringProp name="RegexExtractor.refname">configurable_product_for_edit_ids</stringProp> - <stringProp name="RegexExtractor.regex">\"id\":(\d+),</stringProp> + <stringProp name="RegexExtractor.regex">\"id\":(\d+),\"sku\"</stringProp> <stringProp name="RegexExtractor.template">$1$</stringProp> <stringProp name="RegexExtractor.default"/> <stringProp name="RegexExtractor.match_number">-1</stringProp> @@ -1793,7 +1793,7 @@ editProductList.add(editProductMap);</stringProp> <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regular Expression Extractor: Extract product ids" enabled="true"> <stringProp name="RegexExtractor.useHeaders">false</stringProp> <stringProp name="RegexExtractor.refname">simple_product_for_edit_ids</stringProp> - <stringProp name="RegexExtractor.regex">\"id\":(\d+),</stringProp> + <stringProp name="RegexExtractor.regex">\"id\":(\d+),\"sku\"</stringProp> <stringProp name="RegexExtractor.template">$1$</stringProp> <stringProp name="RegexExtractor.default"/> <stringProp name="RegexExtractor.match_number">-1</stringProp> @@ -2009,7 +2009,7 @@ editProductList.add(editProductMap);</stringProp> <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regular Expression Extractor: Extract product ids" enabled="true"> <stringProp name="RegexExtractor.useHeaders">false</stringProp> <stringProp name="RegexExtractor.refname">simple_product_ids</stringProp> - <stringProp name="RegexExtractor.regex">\"id\":(\d+),</stringProp> + <stringProp name="RegexExtractor.regex">\"id\":(\d+),\"sku\"</stringProp> <stringProp name="RegexExtractor.template">$1$</stringProp> <stringProp name="RegexExtractor.default"/> <stringProp name="RegexExtractor.match_number">-1</stringProp> @@ -2535,21 +2535,21 @@ if (props.get("category_names_list") == null) { <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true"> <collectionProp name="Arguments.arguments"> <elementProp name="blocks" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">false</boolProp> + <boolProp name="HTTPArgument.always_encode">true</boolProp> <stringProp name="Argument.value">["customer_form_login"]</stringProp> <stringProp name="Argument.metadata">=</stringProp> <boolProp name="HTTPArgument.use_equals">true</boolProp> <stringProp name="Argument.name">blocks</stringProp> </elementProp> <elementProp name="handles" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">false</boolProp> + <boolProp name="HTTPArgument.always_encode">true</boolProp> <stringProp name="Argument.value">["default","customer_account_login"]</stringProp> <stringProp name="Argument.metadata">=</stringProp> <boolProp name="HTTPArgument.use_equals">true</boolProp> <stringProp name="Argument.name">handles</stringProp> </elementProp> <elementProp name="originalRequest" elementType="HTTPArgument"> - <boolProp name="HTTPArgument.always_encode">false</boolProp> + <boolProp name="HTTPArgument.always_encode">true</boolProp> <stringProp name="Argument.value">{"route":"customer","controller":"account","action":"login","uri":"/customer/account/login/"}</stringProp> <stringProp name="Argument.metadata">=</stringProp> <boolProp name="HTTPArgument.use_equals">true</boolProp>