diff --git a/etc/behat/services/contexts/transform.xml b/etc/behat/services/contexts/transform.xml index ee7ed74b9a..95acb913ed 100644 --- a/etc/behat/services/contexts/transform.xml +++ b/etc/behat/services/contexts/transform.xml @@ -97,6 +97,11 @@ + + + + + diff --git a/etc/behat/services/pages.xml b/etc/behat/services/pages.xml index bc85f5afaa..369e928e6d 100644 --- a/etc/behat/services/pages.xml +++ b/etc/behat/services/pages.xml @@ -59,7 +59,7 @@ %sylius.behat.page.admin.crud.index.class% Sylius\Behat\Page\Admin\Product\Attribute\UpdatePage Sylius\Behat\Page\Admin\ProductOption\CreatePage - %sylius.behat.page.admin.crud.update.class% + Sylius\Behat\Page\Admin\ProductOption\UpdatePage %sylius.behat.page.admin.crud.index.class% Sylius\Behat\Page\Admin\Promotion\CreatePage Sylius\Behat\Page\Admin\Promotion\UpdatePage diff --git a/etc/behat/suites/ui/product/managing_product_options.yml b/etc/behat/suites/ui/product/managing_product_options.yml index e39834a05a..c106f9e5e3 100644 --- a/etc/behat/suites/ui/product/managing_product_options.yml +++ b/etc/behat/suites/ui/product/managing_product_options.yml @@ -8,6 +8,7 @@ default: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.locale + - sylius.behat.context.transform.product_option - sylius.behat.context.transform.shared_storage - sylius.behat.context.setup.channel diff --git a/features/product/managing_product_options/product_option_unique_code_validation.feature b/features/product/managing_product_options/product_option_unique_code_validation.feature new file mode 100644 index 0000000000..ee869f400b --- /dev/null +++ b/features/product/managing_product_options/product_option_unique_code_validation.feature @@ -0,0 +1,21 @@ +@managing_product_options +Feature: Product option unique code validation + In order to uniquely identify product options + As an Administrator + I want to be prevented from adding two product options with the same code + + Background: + Given the store operates on a single channel in "France" + And the store has a product option "T-Shirt size" with a code "t_shirt_size" + And I am logged in as an administrator + + @ui @javascript + Scenario: Trying to add product option with a taken code + Given I want to create a new product option + When I name it "T-Shirt color" in "English (United States)" + And I specify its code as "t_shirt_size" + And I add the option value with code "OV1" and value "S" + And I add the option value with code "OV2" and value "M" + And I try to add it + Then I should be notified that product option with this code already exists + And there should still be only one product option with code "t_shirt_size" diff --git a/features/product/managing_product_options/product_option_validation.feature b/features/product/managing_product_options/product_option_validation.feature new file mode 100644 index 0000000000..ed19a01a9a --- /dev/null +++ b/features/product/managing_product_options/product_option_validation.feature @@ -0,0 +1,65 @@ +@managing_product_options +Feature: Product option validation + In order to avoid making mistakes when managing a product option + As an Administrator + I want to be prevented from adding it without specifying required fields + + Background: + Given the store operates on a single channel in "France" + And the store has a product option "T-Shirt color" with a code "t_shirt_color" + And I am logged in as an administrator + + @ui @javascript + Scenario: Trying to add a new product option without specifying its code + Given I want to create a new product option + When I name it "T-Shirt size" in "English (United States)" + But I do not specify its code + And I add the option value with code "OV1" and value "S" + And I add the option value with code "OV2" and value "M" + And I try to add it + Then I should be notified that code is required + And the product option with name "T-Shirt size" should not be added + + @ui @javascript + Scenario: Trying to add a new product option without specifying its name + Given I want to create a new product option + When I specify its code as "t_shirt_size" + But I do not name it + And I add the option value with code "OV1" and value "S" + And I add the option value with code "OV2" and value "M" + And I try to add it + Then I should be notified that name is required + And the product option with code "t_shirt_size" should not be added + + @ui + Scenario: Trying to remove name from an existing product option + Given I want to modify the "T-Shirt color" product option + When I remove its name from "English (United States)" translation + And I try to save my changes + Then I should be notified that name is required + And this product option should still be named "T-Shirt color" + + @ui + Scenario: Seeing disabled code field when editing product option + Given I want to modify the "T-Shirt color" product option + Then the code field should be disabled + + @ui + Scenario: Trying to add a new product option without any option values + Given I want to create a new product option + When I name it "T-Shirt size" in "English (United States)" + And I specify its code as "t_shirt_size" + But I do not add an option value + And I try to add it + Then I should be notified that at least two option values are required + And the product option with name "T-Shirt size" should not be added + + @ui @javascript + Scenario: Trying to add a new product option with one option value + Given I want to create a new product option + When I name it "T-Shirt size" in "English (United States)" + And I specify its code as "t_shirt_size" + And I add the option value with code "OV1" and value "S" + And I try to add it + Then I should be notified that at least two option values are required + And the product option with name "T-Shirt size" should not be added diff --git a/src/Sylius/Behat/Context/Setup/ProductContext.php b/src/Sylius/Behat/Context/Setup/ProductContext.php index b56fbb5cb3..236ed6e4cf 100644 --- a/src/Sylius/Behat/Context/Setup/ProductContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductContext.php @@ -20,13 +20,14 @@ use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; use Sylius\Component\Core\Test\Services\SharedStorageInterface; use Sylius\Component\Resource\Factory\FactoryInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Taxation\Model\TaxCategoryInterface; +use Sylius\Component\Variation\Repository\OptionRepositoryInterface; /** * @author Arkadiusz Krakowiak * @author Mateusz Zalewski * @author Magdalena Banasiak + * @author Grzegorz Sadowski */ final class ProductContext implements Context { @@ -56,7 +57,7 @@ final class ProductContext implements Context private $productOptionFactory; /** - * @var RepositoryInterface + * @var OptionRepositoryInterface */ private $productOptionRepository; @@ -71,7 +72,7 @@ final class ProductContext implements Context * @param FactoryInterface $productFactory * @param FactoryInterface $productVariantFactory * @param FactoryInterface $productOptionFactory - * @param RepositoryInterface $productOptionRepository + * @param OptionRepositoryInterface $productOptionRepository * @param ObjectManager $objectManager */ public function __construct( @@ -80,7 +81,7 @@ final class ProductContext implements Context FactoryInterface $productFactory, FactoryInterface $productVariantFactory, FactoryInterface $productOptionFactory, - RepositoryInterface $productOptionRepository, + OptionRepositoryInterface $productOptionRepository, ObjectManager $objectManager ) { $this->sharedStorage = $sharedStorage; @@ -186,6 +187,19 @@ final class ProductContext implements Context $this->objectManager->flush($productVariant); } + /** + * @Given the store has a product option :productOptionName with a code :productOptionCode + */ + public function theStoreHasAProductOptionWithACode($productOptionName, $productOptionCode) + { + $productOption = $this->productOptionFactory->createNew(); + $productOption->setCode($productOptionCode); + $productOption->setName($productOptionName); + + $this->sharedStorage->set('product_option', $productOption); + $this->productOptionRepository->add($productOption); + } + /** * @param string $price * diff --git a/src/Sylius/Behat/Context/Transform/ProductOptionContext.php b/src/Sylius/Behat/Context/Transform/ProductOptionContext.php index b15eb9d634..49203dc8e8 100644 --- a/src/Sylius/Behat/Context/Transform/ProductOptionContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductOptionContext.php @@ -12,9 +12,7 @@ namespace Sylius\Behat\Context\Transform; use Behat\Behat\Context\Context; -use Sylius\Component\Core\Repository\ProductRepositoryInterface; -use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; +use Sylius\Component\Variation\Repository\OptionRepositoryInterface; use Webmozart\Assert\Assert; /** @@ -23,14 +21,14 @@ use Webmozart\Assert\Assert; final class ProductOptionContext implements Context { /** - * @var RepositoryInterface + * @var OptionRepositoryInterface */ private $productOptionRepository; /** - * @param RepositoryInterface $productOptionRepository + * @param OptionRepositoryInterface $productOptionRepository */ - public function __construct(RepositoryInterface $productOptionRepository) + public function __construct(OptionRepositoryInterface $productOptionRepository) { $this->productOptionRepository = $productOptionRepository; } @@ -41,7 +39,7 @@ final class ProductOptionContext implements Context */ public function getProductOptionByName($productOptionName) { - $productOption = $this->productOptionRepository->findOneBy(['name' => $productOptionName]); + $productOption = $this->productOptionRepository->findOneByName($productOptionName); Assert::notNull( $productOption, sprintf('Product option with name "%s" does not exist in the product option repository.', $productOptionName) diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductOptionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductOptionsContext.php index ecd195e4cb..f63abf3c20 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductOptionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductOptionsContext.php @@ -14,9 +14,10 @@ namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; use Sylius\Behat\Page\Admin\ProductOption\CreatePageInterface; -use Sylius\Behat\Page\Admin\Crud\UpdatePageInterface; +use Sylius\Behat\Page\Admin\ProductOption\UpdatePageInterface; use Sylius\Behat\Service\CurrentPageResolverInterface; use Sylius\Behat\Service\NotificationCheckerInterface; +use Sylius\Component\Product\Model\OptionInterface; use Webmozart\Assert\Assert; /** @@ -80,6 +81,14 @@ final class ManagingProductOptionsContext implements Context $this->createPage->open(); } + /** + * @Given I want to modify the :productOption product option + */ + public function iWantToModifyAPaymentMethod(OptionInterface $productOption) + { + $this->updatePage->open(['id' => $productOption->getId()]); + } + /** * @When I browse product options */ @@ -108,16 +117,26 @@ final class ManagingProductOptionsContext implements Context /** * @When I name it :name in :language + * @When I remove its name from :language translation */ - public function iNameItInLanguage($name, $language) + public function iNameItInLanguage($name = null, $language) { $this->createPage->nameItIn($name, $language); } /** - * @When I specify its code as :code + * @When I do not name it */ - public function iSpecifyItsCodeAs($code) + public function iDoNotNameIt() + { + // Intentionally left blank to fulfill context expectation + } + + /** + * @When I specify its code as :code + * @When I do not specify its code + */ + public function iSpecifyItsCodeAs($code = null) { $this->createPage->specifyCode($code); } @@ -150,4 +169,107 @@ final class ManagingProductOptionsContext implements Context sprintf('The shipping method with name %s has not been found.', $productOptionName) ); } + + /** + * @Then I should be notified that product option with this code already exists + */ + public function iShouldBeNotifiedThatProductOptionWithThisCodeAlreadyExists() + { + Assert::true( + $this->createPage->checkValidationMessageFor('code', 'The option with given code already exists.'), + 'Unique code violation message should appear on page, but it does not.' + ); + } + + /** + * @Then there should still be only one product option with :element :value + */ + public function thereShouldStillBeOnlyOneProductOptionWith($element, $value) + { + $this->iBrowseProductOptions(); + + Assert::true( + $this->indexPage->isResourceOnPage([$element => $value]), + sprintf('Product option with %s %s cannot be found.', $element, $value) + ); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatElementIsRequired($element) + { + $this->assertFieldValidationMessage($element, sprintf('Please enter option %s.', $element)); + } + + /** + * @Then the product option with :element :value should not be added + */ + public function theProductoptionWithElementValueShouldNotBeAdded($element, $value) + { + $this->iBrowseProductOptions(); + + Assert::false( + $this->indexPage->isResourceOnPage([$element => $value]), + sprintf('Product option with %s %s was created, but it should not.', $element, $value) + ); + } + + /** + * @param string $element + * @param string $expectedMessage + */ + private function assertFieldValidationMessage($element, $expectedMessage) + { + Assert::true( + $this->createPage->checkValidationMessageFor($element, $expectedMessage), + sprintf('Product option %s should be required.', $element) + ); + } + + /** + * @Then /^(this product option) should still be named "([^"]+)"$/ + */ + public function thisProductOptionNameShouldStillBe(OptionInterface $productOption, $productOptionName) + { + $this->iBrowseProductOptions(); + + Assert::true( + $this->indexPage->isResourceOnPage([ + 'code' => $productOption->getCode(), + 'name' => $productOptionName, + ]), + sprintf('Product option name %s has not been assigned properly.', $productOptionName) + ); + } + + /** + * @Then the code field should be disabled + */ + public function theCodeFieldShouldBeDisabled() + { + Assert::true( + $this->updatePage->isCodeDisabled(), + 'Code field should be disabled but it is not' + ); + } + + /** + * @When I do not add an option value + */ + public function iDoNotAddAnOptionValue() + { + // Intentionally left blank to fulfill context expectation + } + + /** + * @Then I should be notified that at least two option values are required + */ + public function iShouldBeNotifiedThatAtLeastTwoOptionValuesAreRequired() + { + Assert::true( + $this->createPage->checkValidationMessageForOptionValues('Please add at least 2 option values.'), + 'I should be notified that product option needs at least two option values.' + ); + } } diff --git a/src/Sylius/Behat/Page/Admin/ProductOption/CreatePage.php b/src/Sylius/Behat/Page/Admin/ProductOption/CreatePage.php index cd310591a9..9655eae9c9 100644 --- a/src/Sylius/Behat/Page/Admin/ProductOption/CreatePage.php +++ b/src/Sylius/Behat/Page/Admin/ProductOption/CreatePage.php @@ -12,6 +12,7 @@ namespace Sylius\Behat\Page\Admin\ProductOption; use Behat\Mink\Element\Element; +use Behat\Mink\Exception\ElementNotFoundException; use Sylius\Behat\Behaviour\SpecifiesItsCode; use Sylius\Behat\Page\Admin\Crud\CreatePage as BaseCreatePage; @@ -45,6 +46,19 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface $optionValueForm->fillField('Value', $code); } + /** + * {@inheritdoc} + */ + public function checkValidationMessageForOptionValues($message) + { + $optionValuesValidationElement = $this->getElement('ui_segment')->find('css', '.ui.pointing'); + if (null === $optionValuesValidationElement) { + throw new ElementNotFoundException($this->getDriver(), 'product option validation box', 'css', '.ui.pointing'); + } + + return $optionValuesValidationElement->getText() === $message; + } + /** * {@inheritdoc} */ @@ -54,6 +68,7 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface 'code' => '#sylius_product_option_code', 'name' => '#sylius_product_option_translations_en_US_name', 'values' => '#sylius_product_option_values', + 'ui_segment' => '.ui.segment', ]); } diff --git a/src/Sylius/Behat/Page/Admin/ProductOption/CreatePageInterface.php b/src/Sylius/Behat/Page/Admin/ProductOption/CreatePageInterface.php index 0eb99523c2..56ba1759d8 100644 --- a/src/Sylius/Behat/Page/Admin/ProductOption/CreatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/ProductOption/CreatePageInterface.php @@ -34,4 +34,9 @@ interface CreatePageInterface extends BaseCreatePageInterface * @param string $value */ public function addOptionValue($code, $value); + + /** + * @param string $message + */ + public function checkValidationMessageForOptionValues($message); } diff --git a/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePage.php b/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePage.php new file mode 100644 index 0000000000..fe86878a16 --- /dev/null +++ b/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePage.php @@ -0,0 +1,53 @@ + + */ +class UpdatePage extends BaseUpdatePage implements UpdatePageInterface +{ + use ChecksCodeImmutability; + + /** + * {@inheritdoc} + */ + public function nameItIn($name, $language) + { + $this->getDocument()->fillField( + sprintf('sylius_product_option_translations_%s_name', $language), $name + ); + } + + /** + * {@inheritdoc} + */ + protected function getCodeElement() + { + return $this->getElement('code'); + } + + /** + * {@inheritdoc} + */ + protected function getDefinedElements() + { + return array_merge(parent::getDefinedElements(), [ + 'code' => '#sylius_product_option_code', + 'name' => '#sylius_product_option_translations_en_US_name', + 'values' => '#sylius_product_option_values', + ]); + } +} diff --git a/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePageInterface.php b/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePageInterface.php new file mode 100644 index 0000000000..40ec608026 --- /dev/null +++ b/src/Sylius/Behat/Page/Admin/ProductOption/UpdatePageInterface.php @@ -0,0 +1,31 @@ + + */ +interface UpdatePageInterface extends BaseUpdatePageInterface +{ + /** + * @return bool + */ + public function isCodeDisabled(); + + /** + * @param string $name + * @param string $languageCode + */ + public function nameItIn($name, $languageCode); +} diff --git a/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml index d1aaa77161..886904513c 100644 --- a/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/UiBundle/Resources/translations/messages.en.yml @@ -36,6 +36,7 @@ sylius: edit_currency: Edit currency edit_customer: Edit customer edit_payment_method: Edit payment method + edit_product_option: Edit product option edit_promotion: Edit promotion edit_tax_category: Edit tax category edit_tax_rate: Edit tax rate