|
30 | 30 |
|
31 | 31 | namespace GlpiPlugin\Field\Tests\Units; |
32 | 32 |
|
| 33 | +use DBmysql; |
33 | 34 | use Glpi\Form\Condition\ConditionHandler\ItemAsTextConditionHandler; |
34 | 35 | use Glpi\Form\Condition\ConditionHandler\ItemConditionHandler; |
35 | 36 | use Glpi\Form\Condition\Engine; |
|
38 | 39 | use Glpi\Form\Condition\Type; |
39 | 40 | use Glpi\Form\Condition\ValueOperator; |
40 | 41 | use Glpi\Form\Condition\VisibilityStrategy; |
| 42 | +use Glpi\Form\Form; |
41 | 43 | use Glpi\Form\Question; |
42 | 44 | use Glpi\Form\QuestionType\QuestionTypeShortText; |
43 | 45 | use Glpi\Form\QuestionType\QuestionTypesManager; |
44 | 46 | use Glpi\Tests\FormBuilder; |
45 | 47 | use GlpiPlugin\Field\Tests\QuestionTypeTestCase; |
46 | 48 | use Location; |
47 | 49 | use LogicException; |
| 50 | +use Migration; |
| 51 | +use PHPUnit\Framework\Attributes\DataProvider; |
48 | 52 | use PluginFieldsContainer; |
49 | 53 | use PluginFieldsDropdown; |
50 | 54 | use PluginFieldsField; |
51 | 55 | use PluginFieldsQuestionType; |
52 | 56 | use PluginFieldsQuestionTypeCategory; |
53 | 57 | use PluginFieldsQuestionTypeExtraDataConfig; |
54 | 58 |
|
| 59 | +use function Safe\json_decode; |
55 | 60 | use function Safe\json_encode; |
56 | 61 |
|
57 | 62 | final class FieldQuestionTypeTest extends QuestionTypeTestCase |
@@ -242,6 +247,194 @@ public function testFormatRawAnswerForMultipleGlpiItemDropdown(): void |
242 | 247 | } |
243 | 248 | } |
244 | 249 |
|
| 250 | + public function testFieldsQuestionEditorRenderingForMultipleDropdownWithStringKeyedDefaultValue(): void |
| 251 | + { |
| 252 | + $this->login(); |
| 253 | + |
| 254 | + // Arrange: create a multiple "dropdown-Location" field |
| 255 | + $this->fields['locations'] = $this->createField([ |
| 256 | + 'label' => 'Locations', |
| 257 | + 'type' => 'dropdown-' . Location::class, |
| 258 | + 'multiple' => 1, |
| 259 | + 'default_value' => [], |
| 260 | + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), |
| 261 | + 'ranking' => 2, |
| 262 | + 'is_active' => 1, |
| 263 | + ], ['default_value']); |
| 264 | + |
| 265 | + $location1 = $this->createItem(Location::class, [ |
| 266 | + 'name' => __FUNCTION__ . ' 1', |
| 267 | + 'entities_id' => $this->getTestRootEntity(true), |
| 268 | + ]); |
| 269 | + $location2 = $this->createItem(Location::class, [ |
| 270 | + 'name' => __FUNCTION__ . ' 2', |
| 271 | + 'entities_id' => $this->getTestRootEntity(true), |
| 272 | + ]); |
| 273 | + |
| 274 | + // Default value with string keys, as they would be passed as named parameters if not reindexed |
| 275 | + $builder = new FormBuilder("My form"); |
| 276 | + $builder->addQuestion( |
| 277 | + "Locations question", |
| 278 | + PluginFieldsQuestionType::class, |
| 279 | + default_value: [ |
| 280 | + 'itemtype' => Location::class, |
| 281 | + 'items_id' => [ |
| 282 | + 'first' => $location1->getID(), |
| 283 | + 'second' => $location2->getID(), |
| 284 | + ], |
| 285 | + ], |
| 286 | + extra_data: json_encode($this->getFieldExtraDataConfig('locations')), |
| 287 | + ); |
| 288 | + $form = $this->createForm($builder); |
| 289 | + |
| 290 | + try { |
| 291 | + // Act: render form editor |
| 292 | + $crawler = $this->renderFormEditor($form); |
| 293 | + |
| 294 | + // Assert: both locations are rendered as the default value |
| 295 | + $selected_values = $crawler |
| 296 | + ->filter('.form-editor-container [data-glpi-form-editor-question] input[name^="default_value[items_id]"]') |
| 297 | + ->each(fn($input) => (int) $input->attr('value')); |
| 298 | + $this->assertEqualsCanonicalizing( |
| 299 | + [$location1->getID(), $location2->getID()], |
| 300 | + $selected_values, |
| 301 | + ); |
| 302 | + } finally { |
| 303 | + // delete locations for another run |
| 304 | + $location1->delete($location1->fields, true); |
| 305 | + $location2->delete($location2->fields, true); |
| 306 | + } |
| 307 | + } |
| 308 | + |
| 309 | + public static function cloneDefaultValueProvider(): iterable |
| 310 | + { |
| 311 | + yield 'text' => ['text', 'hello']; |
| 312 | + yield 'textarea' => ['textarea', "first line\nsecond line"]; |
| 313 | + yield 'number' => ['number', '42']; |
| 314 | + yield 'url' => ['url', 'https://glpi-project.org']; |
| 315 | + yield 'date' => ['date', '2026-10-15']; |
| 316 | + yield 'json-like text' => ['text', '[1,2]']; |
| 317 | + } |
| 318 | + |
| 319 | + #[DataProvider('cloneDefaultValueProvider')] |
| 320 | + public function testCloneKeepsScalarDefaultValue(string $type, string $default_value): void |
| 321 | + { |
| 322 | + $this->login(); |
| 323 | + |
| 324 | + // Arrange: create a form with a question using a scalar field |
| 325 | + $this->fields['scalar'] = $this->createField([ |
| 326 | + 'label' => 'Scalar field', |
| 327 | + 'type' => $type, |
| 328 | + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), |
| 329 | + 'ranking' => 2, |
| 330 | + 'is_active' => 1, |
| 331 | + ]); |
| 332 | + $builder = new FormBuilder("My form"); |
| 333 | + $builder->addQuestion( |
| 334 | + "Scalar question", |
| 335 | + PluginFieldsQuestionType::class, |
| 336 | + default_value: $default_value, |
| 337 | + extra_data: json_encode($this->getFieldExtraDataConfig('scalar')), |
| 338 | + ); |
| 339 | + $form = $this->createForm($builder); |
| 340 | + |
| 341 | + // Act: clone the form, then clone the clone |
| 342 | + $clone = Form::getById($form->clone()); |
| 343 | + $clone_of_clone = Form::getById($clone->clone()); |
| 344 | + |
| 345 | + // Assert: the default value is encoded only once in each form |
| 346 | + foreach ([$form, $clone, $clone_of_clone] as $current_form) { |
| 347 | + $question = Question::getById($this->getQuestionId($current_form, "Scalar question")); |
| 348 | + $this->assertSame(json_encode($default_value), $question->fields['default_value']); |
| 349 | + } |
| 350 | + } |
| 351 | + |
| 352 | + public function testCloneKeepsDropdownDefaultValue(): void |
| 353 | + { |
| 354 | + $this->login(); |
| 355 | + |
| 356 | + // Arrange: create a form with a dropdown question having a default value |
| 357 | + $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); |
| 358 | + $option = $this->createItem($itemtype, ['name' => 'Default option']); |
| 359 | + $default_value = ['itemtype' => $itemtype, 'items_id' => $option->getID()]; |
| 360 | + |
| 361 | + $builder = new FormBuilder("My form"); |
| 362 | + $builder->addQuestion( |
| 363 | + "Dropdown question", |
| 364 | + PluginFieldsQuestionType::class, |
| 365 | + default_value: $default_value, |
| 366 | + extra_data: json_encode($this->getFieldExtraDataConfig('dropdown')), |
| 367 | + ); |
| 368 | + $form = $this->createForm($builder); |
| 369 | + |
| 370 | + // Act: clone the form, then clone the clone |
| 371 | + $clone = Form::getById($form->clone()); |
| 372 | + $clone_of_clone = Form::getById($clone->clone()); |
| 373 | + |
| 374 | + // Assert: the default value is kept as an array in each form |
| 375 | + foreach ([$form, $clone, $clone_of_clone] as $current_form) { |
| 376 | + $question = Question::getById($this->getQuestionId($current_form, "Dropdown question")); |
| 377 | + $this->assertSame($default_value, json_decode($question->fields['default_value'], true)); |
| 378 | + } |
| 379 | + } |
| 380 | + |
| 381 | + public function testInstallRepairsDefaultValuesEncodedByClones(): void |
| 382 | + { |
| 383 | + /** @var DBmysql $DB */ |
| 384 | + global $DB; |
| 385 | + |
| 386 | + $this->login(); |
| 387 | + |
| 388 | + // Arrange: create questions whose default values were encoded by successive clones |
| 389 | + $this->fields['text'] = $this->createField([ |
| 390 | + 'label' => 'Text field', |
| 391 | + 'type' => 'text', |
| 392 | + PluginFieldsContainer::getForeignKeyField() => $this->block->getID(), |
| 393 | + 'ranking' => 2, |
| 394 | + 'is_active' => 1, |
| 395 | + ]); |
| 396 | + $itemtype = PluginFieldsDropdown::getClassname($this->fields['dropdown']->fields['name']); |
| 397 | + $dropdown_value = ['itemtype' => $itemtype, 'items_id' => 1]; |
| 398 | + |
| 399 | + $cases = [ |
| 400 | + // question name => [field, stored default value, expected default value] |
| 401 | + 'Text cloned twice' => ['text', json_encode(json_encode(json_encode('hello'))), json_encode('hello')], |
| 402 | + 'Text not cloned' => ['text', json_encode('hello'), json_encode('hello')], |
| 403 | + 'JSON-like text' => ['text', json_encode('[1,2]'), json_encode('[1,2]')], |
| 404 | + 'Dropdown cloned twice' => ['dropdown', json_encode(json_encode(json_encode($dropdown_value))), json_encode($dropdown_value)], |
| 405 | + 'Dropdown not cloned' => ['dropdown', json_encode($dropdown_value), json_encode($dropdown_value)], |
| 406 | + ]; |
| 407 | + |
| 408 | + $builder = new FormBuilder("My form"); |
| 409 | + foreach ($cases as $name => [$field_name]) { |
| 410 | + $builder->addQuestion( |
| 411 | + $name, |
| 412 | + PluginFieldsQuestionType::class, |
| 413 | + extra_data: json_encode($this->getFieldExtraDataConfig($field_name)), |
| 414 | + ); |
| 415 | + } |
| 416 | + |
| 417 | + $form = $this->createForm($builder); |
| 418 | + foreach ($cases as $name => [, $stored_value]) { |
| 419 | + $DB->update( |
| 420 | + Question::getTable(), |
| 421 | + ['default_value' => $stored_value], |
| 422 | + ['id' => $this->getQuestionId($form, $name)], |
| 423 | + ); |
| 424 | + } |
| 425 | + |
| 426 | + // Act: run the plugin migration |
| 427 | + ob_start(); |
| 428 | + PluginFieldsQuestionType::installBaseData(new Migration(PLUGIN_FIELDS_VERSION), PLUGIN_FIELDS_VERSION); |
| 429 | + ob_end_clean(); |
| 430 | + |
| 431 | + // Assert: default values are encoded only once |
| 432 | + foreach ($cases as $name => [, , $expected_value]) { |
| 433 | + $question = Question::getById($this->getQuestionId($form, $name)); |
| 434 | + $this->assertSame($expected_value, $question->fields['default_value'], $name); |
| 435 | + } |
| 436 | + } |
| 437 | + |
245 | 438 | public function testFieldDeletionWhenUsedInForm(): void |
246 | 439 | { |
247 | 440 | $this->login(); |
|
0 commit comments