diff --git a/CHANGELOG.md b/CHANGELOG.md index 5539b04..39b1625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Fixed + +- Fix string condition operators (equals, contains, length) not being available on hidden questions + ## [1.2.0] - 2026-07-28 ### Add diff --git a/src/Model/QuestionType/HiddenQuestion.php b/src/Model/QuestionType/HiddenQuestion.php index f59c6f0..cf205da 100644 --- a/src/Model/QuestionType/HiddenQuestion.php +++ b/src/Model/QuestionType/HiddenQuestion.php @@ -34,6 +34,9 @@ namespace GlpiPlugin\Advancedforms\Model\QuestionType; use Glpi\Application\View\TemplateRenderer; +use Glpi\DBAL\JsonFieldInterface; +use Glpi\Form\Condition\ConditionHandler\StringConditionHandler; +use Glpi\Form\Condition\UsedAsCriteriaInterface; use Glpi\Form\Migration\FormQuestionDataConverterInterface; use Glpi\Form\Question; use Glpi\Form\QuestionType\AbstractQuestionType; @@ -46,7 +49,10 @@ * Legacy question type from the formcreator plugin * Original source: https://github.com/pluginsGLPI/formcreator/blob/2.13.10/inc/field/hiddenfield.class.php */ -final class HiddenQuestion extends AbstractQuestionType implements ConfigurableItemInterface, LegacyQuestionTypeInterface +final class HiddenQuestion extends AbstractQuestionType implements + ConfigurableItemInterface, + LegacyQuestionTypeInterface, + UsedAsCriteriaInterface { #[Override] public function getCategory(): QuestionTypeCategoryInterface @@ -157,4 +163,11 @@ public function getMapperClass(): FormQuestionDataConverterInterface { return new FormcreatorHiddenTypeMapper(); } + + #[Override] + public function getConditionHandlers( + ?JsonFieldInterface $question_config, + ): array { + return array_merge(parent::getConditionHandlers($question_config), [new StringConditionHandler()]); + } } diff --git a/tests/AdvancedFormsTestCase.php b/tests/AdvancedFormsTestCase.php index ecf9c4f..e259224 100644 --- a/tests/AdvancedFormsTestCase.php +++ b/tests/AdvancedFormsTestCase.php @@ -34,7 +34,6 @@ namespace GlpiPlugin\Advancedforms\Tests; use AuthLDAP; -use Config; use Glpi\Form\Form; use Glpi\Form\Migration\TypesConversionMapper; use Glpi\Form\QuestionType\QuestionTypeInterface; @@ -46,13 +45,11 @@ use GlpiPlugin\Advancedforms\Model\QuestionType\LdapQuestion; use GlpiPlugin\Advancedforms\Model\QuestionType\LdapQuestionConfig; use GlpiPlugin\Advancedforms\Service\ConfigManager; -use GlpiPlugin\Advancedforms\Service\InitManager; -use InvalidArgumentException; -use ReflectionClass; abstract class AdvancedFormsTestCase extends DbTestCase { use FormTesterTrait; + use ConfigurableItemsTrait; /** @return array */ final public static function provideQuestionTypes(): array @@ -72,42 +69,6 @@ public function setUp(): void ]); } - protected function enableConfigurableItem( - ConfigurableItemInterface|string $item, - ): void { - $this->setConfigurableItemConfig($item, true); - InitManager::getInstance()->init(); - } - - /** @var array $items */ - protected function enableConfigurableItems( - array $items, - ): void { - foreach ($items as $item) { - $this->setConfigurableItemConfig($item, true); - } - - InitManager::getInstance()->init(); - } - - protected function disableConfigurableItem( - ConfigurableItemInterface|string $item, - ): void { - $this->setConfigurableItemConfig($item, false); - InitManager::getInstance()->init(); - } - - /** @var array $items */ - protected function disableConfigurableItems( - array $items, - ): void { - foreach ($items as $item) { - $this->setConfigurableItemConfig($item, false); - } - - InitManager::getInstance()->init(); - } - protected function setupAuthLdap(): AuthLDAP { return $this->createItem(AuthLDAP::class, [ @@ -141,36 +102,4 @@ protected function createFormWithLdapQuestion(AuthLdap $ldap): Form ); return $this->createForm($builder); } - - private function setConfigurableItemConfig( - ConfigurableItemInterface|string $item, - bool $enabled, - ): void { - if ( - is_string($item) - && !is_a($item, ConfigurableItemInterface::class, true) - ) { - throw new InvalidArgumentException(); - } - - Config::setConfigurationValues('advancedforms', [ - $item::getConfigKey() => (int) $enabled, - ]); - } - - private function deleteSingletonInstance(array $classes) - { - foreach ($classes as $class) { - $reflection_class = new ReflectionClass($class); - if ($reflection_class->hasProperty('instance')) { - $reflection_property = $reflection_class->getProperty('instance'); - $reflection_property->setValue(null, null); - } - - if ($reflection_class->hasProperty('_instances')) { - $reflection_property = $reflection_class->getProperty('_instances'); - $reflection_property->setValue(null, []); - } - } - } } diff --git a/tests/ConfigurableItemsTrait.php b/tests/ConfigurableItemsTrait.php new file mode 100644 index 0000000..935430d --- /dev/null +++ b/tests/ConfigurableItemsTrait.php @@ -0,0 +1,119 @@ +setConfigurableItemConfig($item, true); + InitManager::getInstance()->init(); + } + + /** @var array $items */ + protected function enableConfigurableItems( + array $items, + ): void { + foreach ($items as $item) { + $this->setConfigurableItemConfig($item, true); + } + + InitManager::getInstance()->init(); + } + + protected function disableConfigurableItem( + ConfigurableItemInterface|string $item, + ): void { + $this->setConfigurableItemConfig($item, false); + InitManager::getInstance()->init(); + } + + /** @var array $items */ + protected function disableConfigurableItems( + array $items, + ): void { + foreach ($items as $item) { + $this->setConfigurableItemConfig($item, false); + } + + InitManager::getInstance()->init(); + } + + private function setConfigurableItemConfig( + ConfigurableItemInterface|string $item, + bool $enabled, + ): void { + if ( + is_string($item) + && !is_a($item, ConfigurableItemInterface::class, true) + ) { + throw new InvalidArgumentException(); + } + + Config::setConfigurationValues('advancedforms', [ + $item::getConfigKey() => (int) $enabled, + ]); + } + + /** @var array $classes */ + private function deleteSingletonInstance(array $classes) + { + foreach ($classes as $class) { + $reflection_class = new ReflectionClass($class); + if ($reflection_class->hasProperty('instance')) { + $reflection_property = $reflection_class->getProperty('instance'); + $reflection_property->setValue(null, null); + } + + if ($reflection_class->hasProperty('_instances')) { + $reflection_property = $reflection_class->getProperty('_instances'); + $reflection_property->setValue(null, []); + } + } + } +} diff --git a/tests/Model/ConditionHandler/HiddenQuestionStringConditionHandlerTest.php b/tests/Model/ConditionHandler/HiddenQuestionStringConditionHandlerTest.php new file mode 100644 index 0000000..a2d09cc --- /dev/null +++ b/tests/Model/ConditionHandler/HiddenQuestionStringConditionHandlerTest.php @@ -0,0 +1,353 @@ +deleteSingletonInstance([ + QuestionTypesManager::class, + TypesConversionMapper::class, + ]); + + $this->enableConfigurableItem(HiddenQuestion::class); + } + + public static function getConditionHandler(): ConditionHandlerInterface + { + return new StringConditionHandler(); + } + + /** + * The operators evaluated below must also be offered by the question type + * itself, otherwise they can't be selected in the form editor. + */ + public function testStringOperatorsAreAvailableOnHiddenQuestions(): void + { + $available_operators = (new HiddenQuestion())->getSupportedValueOperators(null); + + foreach (self::getConditionHandler()->getSupportedValueOperators() as $operator) { + $this->assertContains($operator, $available_operators); + } + } + + #[Override] + public static function conditionHandlerProvider(): iterable + { + $type = HiddenQuestion::class; + + // Test hidden values with the EQUALS operator + yield 'Equals check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "standard_customer", + 'expected_result' => false, + ]; + yield 'Equals check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "premium", + 'expected_result' => false, + ]; + yield 'Equals check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "premium_customer", + 'expected_result' => true, + ]; + yield 'Equals check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "PREMIUM_Customer", + 'expected_result' => true, + ]; + yield 'Equals check - case 5' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "", + 'expected_result' => false, + ]; + + // Test hidden values with the NOT_EQUALS operator + yield 'Not equals check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "standard_customer", + 'expected_result' => true, + ]; + yield 'Not equals check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "premium", + 'expected_result' => true, + ]; + yield 'Not equals check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "premium_customer", + 'expected_result' => false, + ]; + yield 'Not equals check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_EQUALS, + 'condition_value' => "premium_customer", + 'submitted_answer' => "PREMIUM_Customer", + 'expected_result' => false, + ]; + + // Test hidden values with the CONTAINS operator + yield 'Contains check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::CONTAINS, + 'condition_value' => "premium", + 'submitted_answer' => "standard_customer", + 'expected_result' => false, + ]; + yield 'Contains check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::CONTAINS, + 'condition_value' => "premium", + 'submitted_answer' => "premium_customer", + 'expected_result' => true, + ]; + yield 'Contains check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::CONTAINS, + 'condition_value' => "customer", + 'submitted_answer' => "premium_customer", + 'expected_result' => true, + ]; + yield 'Contains check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::CONTAINS, + 'condition_value' => "PREMIUM", + 'submitted_answer' => "premium_customer", + 'expected_result' => true, + ]; + + // Test hidden values with the NOT_CONTAINS operator + yield 'Not contains check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_CONTAINS, + 'condition_value' => "premium", + 'submitted_answer' => "standard_customer", + 'expected_result' => true, + ]; + yield 'Not contains check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_CONTAINS, + 'condition_value' => "premium", + 'submitted_answer' => "premium_customer", + 'expected_result' => false, + ]; + yield 'Not contains check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::NOT_CONTAINS, + 'condition_value' => "PREMIUM", + 'submitted_answer' => "premium_customer", + 'expected_result' => false, + ]; + + // Test hidden values with the LENGTH_GREATER_THAN operator + yield 'Length greater than check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN, + 'condition_value' => 10, + 'submitted_answer' => "short", + 'expected_result' => false, + ]; + yield 'Length greater than check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN, + 'condition_value' => 10, + 'submitted_answer' => "premium_customer", + 'expected_result' => true, + ]; + yield 'Length greater than check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN, + 'condition_value' => 10, + 'submitted_answer' => "exactlyten", + 'expected_result' => false, + ]; + yield 'Length greater than check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN, + 'condition_value' => 2, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => true, + ]; + yield 'Length greater than check - case 5' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN, + 'condition_value' => 3, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => false, + ]; + + // Test hidden values with the LENGTH_GREATER_THAN_OR_EQUALS operator + yield 'Length greater than or equals check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN_OR_EQUALS, + 'condition_value' => 10, + 'submitted_answer' => "short", + 'expected_result' => false, + ]; + yield 'Length greater than or equals check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN_OR_EQUALS, + 'condition_value' => 10, + 'submitted_answer' => "exactlyten", + 'expected_result' => true, + ]; + yield 'Length greater than or equals check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN_OR_EQUALS, + 'condition_value' => 3, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => true, + ]; + yield 'Length greater than or equals check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_GREATER_THAN_OR_EQUALS, + 'condition_value' => 4, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => false, + ]; + + // Test hidden values with the LENGTH_LESS_THAN operator + yield 'Length less than check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN, + 'condition_value' => 10, + 'submitted_answer' => "premium_customer", + 'expected_result' => false, + ]; + yield 'Length less than check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN, + 'condition_value' => 10, + 'submitted_answer' => "short", + 'expected_result' => true, + ]; + yield 'Length less than check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN, + 'condition_value' => 10, + 'submitted_answer' => "exactlyten", + 'expected_result' => false, + ]; + yield 'Length less than check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN, + 'condition_value' => 4, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => true, + ]; + yield 'Length less than check - case 5' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN, + 'condition_value' => 3, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => false, + ]; + + // Test hidden values with the LENGTH_LESS_THAN_OR_EQUALS operator + yield 'Length less than or equals check - case 1' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN_OR_EQUALS, + 'condition_value' => 10, + 'submitted_answer' => "premium_customer", + 'expected_result' => false, + ]; + yield 'Length less than or equals check - case 2' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN_OR_EQUALS, + 'condition_value' => 10, + 'submitted_answer' => "exactlyten", + 'expected_result' => true, + ]; + yield 'Length less than or equals check - case 3' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN_OR_EQUALS, + 'condition_value' => 3, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => true, + ]; + yield 'Length less than or equals check - case 4' => [ + 'question_type' => $type, + 'condition_operator' => ValueOperator::LENGTH_LESS_THAN_OR_EQUALS, + 'condition_value' => 2, + 'submitted_answer' => "für", // multi byte string + 'expected_result' => false, + ]; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 38d5ab0..4716ab6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -39,6 +39,7 @@ throw new RuntimeException("Plugin advancedforms is not active in the test database"); } +require __DIR__ . "/ConfigurableItemsTrait.php"; require __DIR__ . "/AdvancedFormsTestCase.php"; require __DIR__ . "/Front/FrontTestCase.php"; require __DIR__ . "/Model/Mapper/MapperTestCase.php";