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