diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php b/src/Plugin/ExperienceBuilder/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php index 8393dc4ea5f5e015c7d2d366c67b8b5581e0c972..f94837d7ef732622981506894787be12355c16d0 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php @@ -123,7 +123,12 @@ abstract class GeneratedFieldExplicitInputUxComponentSourceBase extends Componen $dependencies['module'][] = $field_widget_definition['provider']; } - return $dependencies; + ksort($dependencies); + return array_map(static function ($values) { + $values = array_unique($values); + sort($values); + return $values; + }, $dependencies); } /** diff --git a/src/PropSource/StaticPropSource.php b/src/PropSource/StaticPropSource.php index bd6460d5d390bb4975426267ad7ac45cde819940..ea92626580faabaf9093ef69f44896c85b842d0b 100644 --- a/src/PropSource/StaticPropSource.php +++ b/src/PropSource/StaticPropSource.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\experience_builder\PropSource; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; @@ -481,7 +482,26 @@ final class StaticPropSource extends PropSourceBase { // calculated dependencies will be limited to the entity types, bundle (if // any) and fields (if any) that this expression depends on. // @see \Drupal\Tests\experience_builder\Kernel\PropExpressionDependenciesTest - return $this->expression->calculateDependencies($this->fieldItemList); + $expression_deps = $this->expression->calculateDependencies($this->fieldItemList); + + // Let the field type plugin specify its own dependencies, based on storage + // settings and instance settings. + $field_item_class = $this->fieldItemList->getItemDefinition()->getClass(); + assert(is_subclass_of($field_item_class, FieldItemInterface::class)); + $instance_deps = $field_item_class::calculateDependencies($this->fieldItemList->getFieldDefinition()); + $storage_deps = $field_item_class::calculateStorageDependencies($this->fieldItemList->getFieldDefinition()->getFieldStorageDefinition()); + + $dependencies = NestedArray::mergeDeep( + $expression_deps, + $instance_deps, + $storage_deps, + ); + ksort($dependencies); + return array_map(static function ($values) { + $values = array_unique($values); + sort($values); + return $values; + }, $dependencies); } } diff --git a/tests/src/Kernel/Config/ComponentValidationTest.php b/tests/src/Kernel/Config/ComponentValidationTest.php index e131cf494b14fbf32d4bd305be53ecc36717e5dd..cf4868b288064e8f0d38eda6d260e7aa8cade364 100644 --- a/tests/src/Kernel/Config/ComponentValidationTest.php +++ b/tests/src/Kernel/Config/ComponentValidationTest.php @@ -12,6 +12,7 @@ use Drupal\experience_builder\Plugin\ComponentPluginManager; use Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent; use Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent; use Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\SingleDirectoryComponent; +use Drupal\Tests\experience_builder\Traits\BetterConfigDependencyManagerTrait; use Drupal\Tests\experience_builder\Traits\ContribStrictConfigSchemaTestTrait; use Drupal\Tests\experience_builder\Traits\GenerateComponentConfigTrait; use Symfony\Component\Yaml\Yaml; @@ -24,6 +25,7 @@ use Symfony\Component\Yaml\Yaml; */ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { + use BetterConfigDependencyManagerTrait; use ContribStrictConfigSchemaTestTrait; use GenerateComponentConfigTrait; @@ -44,6 +46,12 @@ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { 'options', 'path', 'link', + 'field', + 'media', + 'media_library', + 'views', + 'user', + 'filter', ]; /** @@ -70,6 +78,12 @@ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { */ protected function setUp(): void { parent::setUp(); + $this->installEntitySchema('media'); + $this->installEntitySchema('user'); + $this->setInstallProfile('standard'); + $this->installConfig(['media']); + $this->installSchema('file', ['file_usage']); + $this->installEntitySchema('filter_format'); $this->entity = Component::create([ 'id' => 'sdc.sdc_test.my-cta', @@ -108,6 +122,24 @@ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { 'default_value' => NULL, 'expression' => 'ℹ︎list_string␟value', ], + // @todo This will start failing validation in https://www.drupal.org/i/3525759. + 'image' => [ + 'field_type' => 'image', + 'field_storage_settings' => [ + 'target_type' => 'media', + ], + 'field_instance_settings' => [ + 'handler' => 'default:media', + 'handler_settings' => [ + 'target_bundles' => [ + 'image' => 'image', + ], + ], + ], + 'field_widget' => 'media_library_widget', + 'default_value' => [], + 'expression' => 'ℹ︎image␟{src↝entity␜␜entity:file␝uri␞␟url,alt↠alt,width↠width,height↠height}', + ], ], ], 'label' => 'Test', @@ -116,6 +148,35 @@ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { $this->componentPluginManager = $this->container->get(ComponentPluginManager::class); } + /** + * {@inheritdoc} + */ + public function testEntityIsValid(): void { + parent::testEntityIsValid(); + + // Beyond validity, validate config dependencies are computed correctly. + $this->assertSame( + [ + 'module' => [ + 'image', + 'media_library', + 'options', + 'sdc_test', + ], + ], + $this->entity->getDependencies() + ); + $this->assertSame([ + 'module' => [ + 'image', + 'media_library', + 'options', + 'sdc_test', + 'experience_builder', + ], + ], $this->getAllDependencies($this->entity)); + } + /** * @covers `type: experience_builder.component_source_settings.*` * @covers `type: experience_builder.generated_field_explicit_input_ux` @@ -171,7 +232,11 @@ class ComponentValidationTest extends BetterConfigEntityValidationTestBase { 'local_source_id' => 'my-cta', 'prop_field_definitions' => array_diff_key( $this->entity->getSettings()['prop_field_definitions'], - array_flip(['target']), + // Remove the 'target' key to trigger a validation error. + // Remove the 'image' because the property is not in the JS component + // created above. + // @todo Remove "image" from this in https://www.drupal.org/i/3525759. + array_flip(['target', 'image']), ), ], 'label' => 'Test', diff --git a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php index c49e9b253dc12e7de49e716e062542806c3ab7b6..50bc468ed1847660b779d0cfb9a7e297bc5c9063 100644 --- a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php +++ b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php @@ -483,7 +483,6 @@ final class JsComponentTest extends ComponentSourceTestBase { 'js.xb_test_code_components_vanilla_image' => [ 'module' => [ 'image', - 'image', ], 'config' => [ 'experience_builder.js_component.xb_test_code_components_vanilla_image', @@ -497,9 +496,6 @@ final class JsComponentTest extends ComponentSourceTestBase { 'js.xb_test_code_components_with_props' => [ 'module' => [ 'core', - 'core', - 'core', - 'core', ], 'config' => [ 'experience_builder.js_component.xb_test_code_components_with_props', diff --git a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponentTest.php b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponentTest.php index 5ef2e3744ae81071237b5b6318400b7a58d27ffa..c7fa13dc6adfa1556271735abf535a42c9858ce9 100644 --- a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponentTest.php +++ b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/SingleDirectoryComponentTest.php @@ -707,44 +707,38 @@ HTML, self::assertSame([ 'sdc.xb_test_sdc.crash' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.deprecated' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.experimental' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.grid-container' => [ 'module' => [ - 'options', 'core', + 'options', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.image-gallery' => [ 'module' => [ - 'core', 'core', 'image', - 'image', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.image-optional-with-example' => [ 'module' => [ - 'image', 'image', 'xb_test_sdc', ], @@ -752,43 +746,36 @@ HTML, 'sdc.xb_test_sdc.image-optional-with-example-and-additional-prop' => [ 'module' => [ 'core', - 'core', - 'image', 'image', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.image-optional-without-example' => [ 'module' => [ - 'image', 'image', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.image-required-with-example' => [ 'module' => [ - 'image', 'image', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.props-no-slots' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.props-slots' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], ], 'sdc.xb_test_sdc.sparkline' => [ 'module' => [ - 'core', 'core', 'xb_test_sdc', ], diff --git a/tests/src/Kernel/PropSourceTest.php b/tests/src/Kernel/PropSourceTest.php index b883d370f20809bad85e384ad42d2a3075c18d08..a179c90763d63e2d00ad748e332d236da4c8fd8c 100644 --- a/tests/src/Kernel/PropSourceTest.php +++ b/tests/src/Kernel/PropSourceTest.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace Drupal\Tests\experience_builder\Kernel; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Extension\ExtensionPathResolver; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\Plugin\Field\FieldWidget\BooleanCheckboxWidget; +use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget; use Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget; -use Drupal\Core\Extension\ExtensionPathResolver; use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget; +use Drupal\Core\Field\Plugin\Field\FieldWidget\UriWidget; use Drupal\Core\Url; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; use Drupal\datetime_range\Plugin\Field\FieldWidget\DateRangeDatelistWidget; @@ -25,6 +29,7 @@ use Drupal\experience_builder\PropSource\DynamicPropSource; use Drupal\experience_builder\PropSource\PropSource; use Drupal\experience_builder\PropSource\StaticPropSource; use Drupal\KernelTests\KernelTestBase; +use Drupal\media\Entity\MediaType; use Drupal\node\Entity\NodeType; use Drupal\Tests\experience_builder\Traits\ContribStrictConfigSchemaTestTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; @@ -45,124 +50,170 @@ class PropSourceTest extends KernelTestBase { */ protected static $modules = [ 'experience_builder', + 'field', + 'file', 'node', 'user', 'datetime', 'datetime_range', + 'media', + 'media_library', 'system', 'media', + 'views', ]; + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('field_storage_config'); + $this->installEntitySchema('field_config'); + $media_type = MediaType::create([ + 'id' => 'image', + 'label' => 'Image', + 'source' => 'file', + ]); + $media_type->save(); + // Create the source field. + $source_field = $media_type->getSource()->createSourceField($media_type); + $field_storage_definition = $source_field->getFieldStorageDefinition(); + assert($field_storage_definition instanceof EntityInterface); + $field_storage_definition->save(); + + $source_field->save(); + $media_type + ->set('source_configuration', [ + 'source_field' => $source_field->getName(), + ]) + ->save(); + } + /** * @coversClass \Drupal\experience_builder\PropSource\StaticPropSource + * @dataProvider providerStaticPropSource */ - public function testStaticPropSource(): void { - // A simple example. - $simple_example = StaticPropSource::parse([ - 'sourceType' => 'static:field_item:string', - 'value' => 'Hello, world!', - 'expression' => 'ℹ︎string␟value', + public function testStaticPropSource( + string $source_type, + array|null $source_type_settings, + mixed $value, + string $expression, + string $expected_json_representation, + array|null $field_widgets, + mixed $expected_user_value, + string $expected_prop_expression_class, + array $expected_dependencies, + ): void { + // @phpstan-ignore-next-line + $prop_source_example = StaticPropSource::parse([ + 'sourceType' => $source_type, + 'value' => $value, + 'expression' => $expression, + 'sourceTypeSettings' => $source_type_settings, ]); // First, get the string representation and parse it back, to prove // serialization and deserialization works. - $json_representation = (string) $simple_example; - $this->assertSame('{"sourceType":"static:field_item:string","value":"Hello, world!","expression":"ℹ︎string␟value"}', $json_representation); + $json_representation = (string) $prop_source_example; + $this->assertSame($expected_json_representation, $json_representation); $decoded_representation = json_decode($json_representation, TRUE); - try { - StaticPropSource::isMinimalRepresentation($decoded_representation); - } - catch (\LogicException) { - $this->fail("Not a minimal representation: $json_representation."); - } - $simple_example = PropSource::parse($decoded_representation); - $this->assertInstanceOf(StaticPropSource::class, $simple_example); + $prop_source_example = PropSource::parse($decoded_representation); + $this->assertInstanceOf(StaticPropSource::class, $prop_source_example); // The contained information read back out. - $this->assertSame('static:field_item:string', $simple_example->getSourceType()); - $this->assertInstanceOf(FieldTypePropExpression::class, StructuredDataPropExpression::fromString($simple_example->asChoice())); - $this->assertSame('Hello, world!', $simple_example->getValue()); - // Test the functionality of a StaticPropSource: - // - evaluate it to populate an SDC prop - $this->assertSame('Hello, world!', $simple_example->evaluate(User::create([]))); - // - the field type's item's raw value is minimized if it is single-property - $this->assertSame('Hello, world!', $simple_example->getValue()); + $this->assertSame($source_type, $prop_source_example->getSourceType()); + /** @var class-string $expected_prop_expression_class */ + $this->assertInstanceOf($expected_prop_expression_class, StructuredDataPropExpression::fromString($prop_source_example->asChoice())); + self::assertSame($expected_dependencies, $prop_source_example->calculateDependencies()); // - generate a widget to edit the stored value — using the default widget // or a specified widget. // @see \Drupal\experience_builder\Entity\Component::$defaults - $this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test', $this->randomString(), NULL)); - $this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test', $this->randomString(), 'string_textfield')); - // The widget plugin manager ignores any request for another widget type and - // falls back to the default widget if - // @see \Drupal\Core\Field\WidgetPluginManager::getInstance() - $this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test', $this->randomString(), 'string_textarea')); - self::assertSame([ - 'plugin' => [ - 'field_type:string', - ], - ], $simple_example->calculateDependencies()); + \assert(is_array($field_widgets)); + // Ensure we always test the default widget. + \assert(isset($field_widgets[NULL])); + // Ensure an unknown widget type is handled gracefully. + $field_widgets['not_real'] = $field_widgets[NULL]; + foreach ($field_widgets as $widget_type => $expected_widget_class) { + $this->assertInstanceOf($expected_widget_class, $prop_source_example->getWidget('irrelevant-for-test', $this->randomString(), $widget_type)); + } + if (NULL === $value) { + $this->assertNull($expected_user_value); + // Do not continue testing if there is no values. + return; + } - // A complex example. - $complex_example = StaticPropSource::parse([ - 'sourceType' => 'static:field_item:daterange', - 'value' => [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-07-10T10:24', - ], - 'expression' => 'ℹ︎daterange␟{start↠value,stop↠end_value}', - ]); - // First, get the string representation and parse it back, to prove - // serialization and deserialization works. - $json_representation = (string) $complex_example; - $this->assertSame('{"sourceType":"static:field_item:daterange","value":{"value":"2020-04-16T00:00","end_value":"2024-07-10T10:24"},"expression":"ℹ︎daterange␟{start↠value,stop↠end_value}"}', $json_representation); - $decoded_representation = json_decode($json_representation, TRUE); try { StaticPropSource::isMinimalRepresentation($decoded_representation); } catch (\LogicException) { $this->fail("Not a minimal representation: $json_representation."); } - $complex_example = PropSource::parse($decoded_representation); - $this->assertInstanceOf(StaticPropSource::class, $complex_example); - // The contained information read back out. - $this->assertSame('static:field_item:daterange', $complex_example->getSourceType()); - $this->assertInstanceOf(FieldTypeObjectPropsExpression::class, StructuredDataPropExpression::fromString($complex_example->asChoice())); - $this->assertSame([ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-07-10T10:24', - ], $complex_example->getValue()); + $this->assertSame($value, $prop_source_example->getValue()); // Test the functionality of a StaticPropSource: // - evaluate it to populate an SDC prop - $this->assertSame([ - 'start' => '2020-04-16T00:00', - 'stop' => '2024-07-10T10:24', - ], $complex_example->evaluate(User::create([]))); + $this->assertSame($expected_user_value, $prop_source_example->evaluate(User::create([]))); // - the field type's item's raw value is minimized if it is single-property - $this->assertSame( - [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-07-10T10:24', + $this->assertSame($value, $prop_source_example->getValue()); + } + + public static function providerStaticPropSource(): \Generator { + yield "scalar shape, field type=string, cardinality=1" => [ + 'sourceType' => 'static:field_item:string', + 'sourceTypeSettings' => NULL, + 'value' => 'Hello, world!', + 'expression' => 'ℹ︎string␟value', + 'expected_json_representation' => '{"sourceType":"static:field_item:string","value":"Hello, world!","expression":"ℹ︎string␟value"}', + 'field_widgets' => [ + NULL => StringTextfieldWidget::class, + 'string_textfield' => StringTextfieldWidget::class, + 'string_textarea' => StringTextfieldWidget::class, + ], + 'expected_user_value' => 'Hello, world!', + 'expected_prop_expression' => FieldTypePropExpression::class, + 'expected_dependencies' => [ + 'plugin' => [ + 'field_type:string', + ], ], - $complex_example->getValue() - ); - // - generate a widget to edit the stored value — using the default widget - // or a specified widget. - // @see \Drupal\experience_builder\Entity\Component::$defaults - $this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_example->getWidget('irrelevant-for-test', $this->randomString(), NULL)); - $this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_example->getWidget('irrelevant-for-test', $this->randomString(), 'daterange_default')); - $this->assertInstanceOf(DateRangeDatelistWidget::class, $complex_example->getWidget('irrelevant-for-test', $this->randomString(), 'daterange_datelist')); - self::assertSame([ - 'module' => [ - 'datetime_range', - 'datetime_range', + ]; + yield "scalar shape, field type=uri, cardinality=1" => [ + 'sourceType' => 'static:field_item:uri', + 'sourceTypeSettings' => NULL, + 'value' => 'https://drupal.org', + 'expression' => 'ℹ︎uri␟value', + 'expected_json_representation' => '{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"ℹ︎uri␟value"}', + 'field_widgets' => [ + NULL => UriWidget::class, + 'uri' => UriWidget::class, + ], + 'expected_user_value' => 'https://drupal.org', + 'expected_prop_expression' => FieldTypePropExpression::class, + 'expected_dependencies' => [ + 'plugin' => [ + 'field_type:uri', + ], ], - 'plugin' => [ - 'field_type:daterange', - 'field_type:daterange', + ]; + yield "scalar shape, field type=boolean, cardinality=1" => [ + 'sourceType' => 'static:field_item:boolean', + 'sourceTypeSettings' => NULL, + 'value' => TRUE, + 'expression' => 'ℹ︎boolean␟value', + 'expected_json_representation' => '{"sourceType":"static:field_item:boolean","value":true,"expression":"ℹ︎boolean␟value"}', + 'field_widgets' => [ + NULL => BooleanCheckboxWidget::class, + 'boolean_checkbox' => BooleanCheckboxWidget::class, + ], + 'expected_user_value' => TRUE, + 'expected_prop_expression' => FieldTypePropExpression::class, + 'expected_dependencies' => [ + 'plugin' => [ + 'field_type:boolean', + ], ], - ], $complex_example->calculateDependencies()); - + ]; // A simple (expression targeting a simple prop) array example (with // cardinality specified, rather than the default of `cardinality=1`). - $simple_array_example = StaticPropSource::parse([ + yield "scalar shape, field type=integer, cardinality=5" => [ 'sourceType' => 'static:field_item:integer', 'sourceTypeSettings' => [ 'cardinality' => 5, @@ -175,42 +226,56 @@ class PropSourceTest extends KernelTestBase { 92, ], 'expression' => 'ℹ︎integer␟value', - ]); - // First, get the string representation and parse it back, to prove - // serialization and deserialization works. - $json_representation = (string) $simple_array_example; - $this->assertSame('{"sourceType":"static:field_item:integer","value":[20,6,1,88,92],"expression":"ℹ︎integer␟value","sourceTypeSettings":{"cardinality":5}}', $json_representation); - $decoded_representation = json_decode($json_representation, TRUE); - try { - StaticPropSource::isMinimalRepresentation($decoded_representation); - } - catch (\LogicException) { - $this->fail("Not a minimal representation: $json_representation."); - } - $simple_array_example = PropSource::parse($decoded_representation); - $this->assertInstanceOf(StaticPropSource::class, $simple_array_example); - // The contained information read back out. - $this->assertSame('static:field_item:integer', $simple_array_example->getSourceType()); - $this->assertInstanceOf(FieldTypePropExpression::class, StructuredDataPropExpression::fromString($simple_array_example->asChoice())); - $this->assertSame([20, 06, 1, 88, 92], $simple_array_example->getValue()); - // Test the functionality of a StaticPropSource: - // - evaluate it to populate an SDC prop - $this->assertSame([20, 06, 1, 88, 92], $simple_array_example->evaluate(User::create([]))); - // - the field type's item's raw value is minimized if it is single-property - $this->assertSame([20, 06, 1, 88, 92], $simple_array_example->getValue()); - // - generate a widget to edit the stored value — using the default widget - // or a specified widget. - // @see \Drupal\experience_builder\Entity\Component::$defaults - $this->assertInstanceOf(NumberWidget::class, $simple_array_example->getWidget('irrelevant-for-test', $this->randomString(), NULL)); - $this->assertInstanceOf(NumberWidget::class, $simple_array_example->getWidget('irrelevant-for-test', $this->randomString(), 'number')); - // The widget plugin manager ignores any request for another widget type and - // falls back to the default widget if - // @see \Drupal\Core\Field\WidgetPluginManager::getInstance() - $this->assertInstanceOf(NumberWidget::class, $simple_array_example->getWidget('irrelevant-for-test', $this->randomString(), 'number')); - + 'expected_json_representation' => '{"sourceType":"static:field_item:integer","value":[20,6,1,88,92],"expression":"ℹ︎integer␟value","sourceTypeSettings":{"cardinality":5}}', + 'field_widgets' => [ + NULL => NumberWidget::class, + 'number' => NumberWidget::class, + ], + 'expected_user_value' => [ + 20, + 06, + 1, + 88, + 92, + ], + 'expected_prop_expression' => FieldTypePropExpression::class, + 'expected_dependencies' => [ + 'plugin' => [ + 'field_type:integer', + ], + ], + ]; + yield "object shape, daterange field, cardinality=1" => [ + 'sourceType' => 'static:field_item:daterange', + 'sourceTypeSettings' => NULL, + 'value' => [ + 'value' => '2020-04-16T00:00', + 'end_value' => '2024-07-10T10:24', + ], + 'expression' => 'ℹ︎daterange␟{start↠value,stop↠end_value}', + 'expected_json_representation' => '{"sourceType":"static:field_item:daterange","value":{"value":"2020-04-16T00:00","end_value":"2024-07-10T10:24"},"expression":"ℹ︎daterange␟{start↠value,stop↠end_value}"}', + 'field_widgets' => [ + NULL => DateRangeDefaultWidget::class, + 'daterange_default' => DateRangeDefaultWidget::class, + 'daterange_datelist' => DateRangeDatelistWidget::class, + ], + 'expected_user_value' => [ + 'start' => '2020-04-16T00:00', + 'stop' => '2024-07-10T10:24', + ], + 'expected_prop_expression' => FieldTypeObjectPropsExpression::class, + 'expected_dependencies' => [ + 'module' => [ + 'datetime_range', + ], + 'plugin' => [ + 'field_type:daterange', + ], + ], + ]; // A complex (expression targeting multiple props) array example (with // cardinality specified, rather than the default of `cardinality=1`). - $complex_array_example = StaticPropSource::parse([ + yield "object shape, daterange field, cardinality=UNLIMITED" => [ 'sourceType' => 'static:field_item:daterange', 'sourceTypeSettings' => [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, @@ -226,65 +291,66 @@ class PropSourceTest extends KernelTestBase { ], ], 'expression' => 'ℹ︎daterange␟{start↠value,stop↠end_value}', - ]); - // First, get the string representation and parse it back, to prove - // serialization and deserialization works. - $json_representation = (string) $complex_array_example; - $this->assertSame('{"sourceType":"static:field_item:daterange","value":[{"value":"2020-04-16T00:00","end_value":"2024-07-10T10:24"},{"value":"2020-04-16T00:00","end_value":"2024-09-26T11:31"}],"expression":"ℹ︎daterange␟{start↠value,stop↠end_value}","sourceTypeSettings":{"cardinality":-1}}', $json_representation); - $decoded_representation = json_decode($json_representation, TRUE); - try { - StaticPropSource::isMinimalRepresentation($decoded_representation); - } - catch (\LogicException) { - $this->fail("Not a minimal representation: $json_representation."); - } - $complex_array_example = PropSource::parse($decoded_representation); - $this->assertInstanceOf(StaticPropSource::class, $complex_array_example); - // The contained information read back out. - $this->assertSame('static:field_item:daterange', $complex_array_example->getSourceType()); - $this->assertInstanceOf(FieldTypeObjectPropsExpression::class, StructuredDataPropExpression::fromString($complex_array_example->asChoice())); - $this->assertSame([ - [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-07-10T10:24', + 'expected_json_representation' => '{"sourceType":"static:field_item:daterange","value":[{"value":"2020-04-16T00:00","end_value":"2024-07-10T10:24"},{"value":"2020-04-16T00:00","end_value":"2024-09-26T11:31"}],"expression":"ℹ︎daterange␟{start↠value,stop↠end_value}","sourceTypeSettings":{"cardinality":-1}}', + 'field_widgets' => [ + NULL => DateRangeDefaultWidget::class, + 'daterange_default' => DateRangeDefaultWidget::class, + 'daterange_datelist' => DateRangeDatelistWidget::class, ], - [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-09-26T11:31', + 'expected_user_value' => [ + [ + 'start' => '2020-04-16T00:00', + 'stop' => '2024-07-10T10:24', + ], + [ + 'start' => '2020-04-16T00:00', + 'stop' => '2024-09-26T11:31', + ], ], - ], $complex_array_example->getValue()); - // Test the functionality of a StaticPropSource: - // - evaluate it to populate an SDC prop - $this->assertSame([ - [ - 'start' => '2020-04-16T00:00', - 'stop' => '2024-07-10T10:24', + 'expected_prop_expression' => FieldTypeObjectPropsExpression::class, + 'expected_dependencies' => [ + 'module' => [ + 'datetime_range', + ], + 'plugin' => [ + 'field_type:daterange', + ], ], - [ - 'start' => '2020-04-16T00:00', - 'stop' => '2024-09-26T11:31', + ]; + yield "complex empty example with entity_reference" => [ + 'sourceType' => 'static:field_item:entity_reference', + 'sourceTypeSettings' => [ + 'storage' => ['target_type' => 'media'], + 'instance' => [ + 'handler' => 'default:media', + 'handler_settings' => [ + 'target_bundles' => ['image' => 'image'], + ], + ], ], - ], $complex_array_example->evaluate(User::create([]))); - // - the field type's item's raw value is minimized if it is single-property - $this->assertSame( - [ - [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-07-10T10:24', + 'value' => NULL, + 'expression' => 'ℹ︎entity_reference␟{src↝entity␜␜entity:media:image␝field_media_image␞␟entity␜␜entity:file␝uri␞␟url,alt↝entity␜␜entity:media:image␝field_media_image␞␟alt,width↝entity␜␜entity:media:image␝field_media_image␞␟width,height↝entity␜␜entity:media:image␝field_media_image␞␟height}', + 'expected_json_representation' => '{"sourceType":"static:field_item:entity_reference","value":null,"expression":"ℹ︎entity_reference␟{src↝entity␜␜entity:media:image␝field_media_image␞␟entity␜␜entity:file␝uri␞␟url,alt↝entity␜␜entity:media:image␝field_media_image␞␟alt,width↝entity␜␜entity:media:image␝field_media_image␞␟width,height↝entity␜␜entity:media:image␝field_media_image␞␟height}","sourceTypeSettings":{"storage":{"target_type":"media"},"instance":{"handler":"default:media","handler_settings":{"target_bundles":{"image":"image"}}}}}', + 'field_widgets' => [ + NULL => EntityReferenceAutocompleteWidget::class, + 'entity_reference_autocomplete' => EntityReferenceAutocompleteWidget::class, + 'daterange_datelist' => EntityReferenceAutocompleteWidget::class, + ], + 'expected_user_value' => NULL, + 'expected_prop_expression' => FieldTypeObjectPropsExpression::class, + 'expected_dependencies' => [ + 'config' => [ + 'media.type.image', ], - [ - 'value' => '2020-04-16T00:00', - 'end_value' => '2024-09-26T11:31', + 'content' => [], + 'module' => [ + 'media', + ], + 'plugin' => [ + 'field_type:entity_reference', ], ], - $complex_array_example->getValue() - ); - // - generate a widget to edit the stored value — using the default widget - // or a specified widget. - // @see \Drupal\experience_builder\Entity\Component::$defaults - $this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_array_example->getWidget('irrelevant-for-test', $this->randomString(), NULL)); - $this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_array_example->getWidget('irrelevant-for-test', $this->randomString(), 'daterange_default')); - $this->assertInstanceOf(DateRangeDatelistWidget::class, $complex_array_example->getWidget('irrelevant-for-test', $this->randomString(), 'daterange_datelist')); + ]; } /** @@ -598,7 +664,7 @@ class PropSourceTest extends KernelTestBase { * @coversClass \Drupal\experience_builder\PropSource\DefaultRelativeUrlPropSource */ public function testDefaultRelativeUrlPropSource(): void { - $this->enableModules(['xb_test_sdc', 'link', 'image', 'file', 'options']); + $this->enableModules(['xb_test_sdc', 'link', 'image', 'options']); // Force rebuilding of the definitions which will create the required // component. $plugin_manager = $this->container->get(ComponentPluginManager::class);