diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml new file mode 100644 index 0000000000000000000000000000000000000000..44008ed49e64972e7fdb2fd661e994cd26ae6523 --- /dev/null +++ b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml @@ -0,0 +1,11 @@ +name: "UI Patterns Test component" +props: + type: object + properties: + string: + title: "String" + type: "string" +slots: + slot: + title: "Slot" + description: "Slot." diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.twig b/tests/modules/ui_patterns_test/components/test-component/test-component.twig new file mode 100644 index 0000000000000000000000000000000000000000..4414413afb6b20bdaf3209e2afb6ac4956fbd308 --- /dev/null +++ b/tests/modules/ui_patterns_test/components/test-component/test-component.twig @@ -0,0 +1,8 @@ +<div id="ui-patterns-test-component"> + <div id="ui-patterns-props-string"> + {{ string }} + </div> + <div id="ui-patterns-slots-slot"> + {{ slot }} + </div> +</div> diff --git a/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc b/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc new file mode 100644 index 0000000000000000000000000000000000000000..6b1ae55cf7285acf441d3c22f1c8c6b997ed25eb --- /dev/null +++ b/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Builds placeholder replacement tokens for entity-related data. + */ + +use Drupal\Core\Render\BubbleableMetadata; + +/** + * Implements hook_token_info(). + */ +function ui_patterns_test_token_info(): array { + $type = [ + 'name' => t('Node Tests'), + 'description' => t('Tokens related to individual content items, or "nodes".'), + 'needs-data' => 'entity_test', + ]; + + // Core tokens for nodes. + $entity['id'] = [ + 'name' => t("Entity ID"), + 'description' => t('The unique ID of the content item, or "entity_test".'), + ]; + + return [ + 'types' => ['node' => $type], + 'tokens' => ['node' => $entity], + ]; +} + +/** + * Implements hook_tokens(). + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function ui_patterns_test_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array { + $replacements = []; + + if ($type == 'node' && !empty($data['node'])) { + $entity = $data['node']; + foreach ($tokens as $name => $original) { + switch ($name) { + // Simple key values on the node. + case 'id': + $replacements[$original] = $entity->id(); + break; + + case 'name': + $replacements[$original] = $entity->name(); + break; + + } + } + } + return $replacements; +} diff --git a/tests/src/Kernel/Source/FieldPropertySourceTest.php b/tests/src/Kernel/Source/FieldPropertySourceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a1046b1ab0f96efd4e26a729c965bbf8f6784d25 --- /dev/null +++ b/tests/src/Kernel/Source/FieldPropertySourceTest.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Kernel\Source; + +use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase; + +/** + * Test FieldPropertySource. + * + * @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\Source\FieldPropertySource + * @group ui_patterns + */ +class FieldPropertySourceTest extends SourcePluginsTestBase { + + /** + * Test Field Property Plugin. + */ + public function testPlugin(): void { + $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml"); + $testSets = $testData->getTestSets(); + foreach ($testSets as $test_set_name => $test_set) { + if (!str_starts_with($test_set_name, 'field_property_')) { + continue; + } + $this->runSourcePluginTest($test_set); + } + } + +} diff --git a/tests/src/Kernel/Source/TextWidgetTest.php b/tests/src/Kernel/Source/TextWidgetTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3f8c294617277aa117ba0c57b1a9613f6dc11e7e --- /dev/null +++ b/tests/src/Kernel/Source/TextWidgetTest.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Kernel\Source; + +use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase; + +/** + * Test SourcePluginManager. + * + * @group ui_patterns + */ +class TextWidgetTest extends SourcePluginsTestBase { + + /** + * Test UI Patterns source core plugins. + */ + public function testPlugin(): void { + $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml"); + $testSets = $testData->getTestSets(); + foreach ($testSets as $test_set_name => $test_set) { + if (!str_starts_with($test_set_name, 'textfield_')) { + continue; + } + $this->runSourcePluginTest($test_set); + } + } + +} diff --git a/tests/src/Kernel/Source/TokenSourceTest.php b/tests/src/Kernel/Source/TokenSourceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2ae721fcaa3d9f63a9d08e4857d5cdb8d66ada83 --- /dev/null +++ b/tests/src/Kernel/Source/TokenSourceTest.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Kernel\Source; + +use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase; + +/** + * Test SourcePluginManager. + * + * @group ui_patterns + */ +class TokenSourceTest extends SourcePluginsTestBase { + + /** + * Test UI Patterns source core plugins. + */ + public function testPlugin(): void { + $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml"); + $testSets = $testData->getTestSets(); + foreach ($testSets as $test_set_name => $test_set) { + if (!str_starts_with($test_set_name, 'token_')) { + continue; + } + $this->runSourcePluginTest($test_set); + } + } + +} diff --git a/tests/src/Kernel/SourcePluginsTestBase.php b/tests/src/Kernel/SourcePluginsTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..37bbdb10fa6af6a6de5214def70052aa3d097f4c --- /dev/null +++ b/tests/src/Kernel/SourcePluginsTestBase.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Kernel; + +use Drupal\Core\Plugin\Context\EntityContext; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\ui_patterns\Traits\RunSourcePluginTestTrait; +use Drupal\Tests\ui_patterns\Traits\TestContentCreationTrait; +use Drupal\Tests\ui_patterns\Traits\TestDataTrait; + +/** + * Base class to test source plugins. + * + * @group ui_patterns + */ +class SourcePluginsTestBase extends KernelTestBase { + + use RunSourcePluginTestTrait; + use TestDataTrait; + use TestContentCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'system', + 'user', + 'text', + 'field', + 'node', + 'ui_patterns', + 'ui_patterns_test', + 'datetime', + 'filter', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->createContentType(); + } + + /** + * {@inheritdoc} + */ + protected function getSourceContexts(array $test_set = []): array { + $context = []; + if (isset($test_set['entity'])) { + $entity = $this->createNode('page', is_array($test_set['entity']) ? $test_set['entity'] : []); + $context['entity'] = EntityContext::fromEntity($entity); + } + return $context; + } + +} diff --git a/tests/src/Traits/RunSourcePluginTestTrait.php b/tests/src/Traits/RunSourcePluginTestTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..c3723fe0e4f346c96139d93922c9bdb1725a3585 --- /dev/null +++ b/tests/src/Traits/RunSourcePluginTestTrait.php @@ -0,0 +1,180 @@ +<?php + +namespace Drupal\Tests\ui_patterns\Traits; + +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\ui_patterns\ComponentPluginManager; +use Drupal\ui_patterns\SourcePluginBase; +use Drupal\ui_patterns\SourcePluginManager; +use function PHPUnit\Framework\assertNotNull; +use function PHPUnit\Framework\assertTrue; + +/** + * Run Source with test trait. + */ +trait RunSourcePluginTestTrait { + + /** + * Return the component manager. + */ + protected function componentManager(): ComponentPluginManager { + return \Drupal::service('plugin.manager.sdc'); + } + + /** + * Returns the source plugin manager. + */ + protected function sourcePluginManager(): SourcePluginManager { + return \Drupal::service('plugin.manager.ui_patterns_source'); + } + + /** + * Returns the source contexts required by the test. + * + * Overwrite this function to add additional contexts. + * + * @param array $test_set + * The test set. + * + * @return array + * Returns all source contexts required by the test. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getSourceContexts(array $test_set): array { + return []; + } + + /** + * Returns defined contexts by context id. + */ + private function getContextsByIds(array $test_set): array { + $contexts = []; + if (isset($test_set['contexts']) && is_array($test_set['contexts'])) { + foreach ($test_set['contexts'] as $context_key => $context_value) { + $contexts[$context_key] = new Context(new ContextDefinition('any'), $context_value); + } + } + return array_merge($contexts, $this->getSourceContexts($test_set)); + } + + /** + * Assert an expected output. + * + * @param array $expected_result + * The expected result. + * @param string $result + * The result. + * @param string $message + * The message. + * + * @throws \RuntimeException + */ + private function assertExpectedOutput(array $expected_result, mixed $result, string $message = ''): void { + if (isset($expected_result['value'])) { + $this->assertEquals($expected_result['value'], $result, $message); + } + elseif (isset($expected_result['regEx'])) { + $this->assertTrue(preg_match($expected_result['regEx'], $result) === 1, $message); + } + else { + throw new \RuntimeException(sprintf('Missing "value" or "regEx" in expected result %s', print_r($expected_result, TRUE))); + } + } + + /** + * Get the type definition of a prop or slot. + * + * @param array $component + * The component definition. + * @param string $prop_or_slot_id + * The prop or slot id. + * + * @return mixed + * The type definition. + */ + private function getComponentPropOrSlotTypeDefinition(array $component, string $prop_or_slot_id): mixed { + $type_definition = NULL; + if (isset($component['props']['properties'][$prop_or_slot_id])) { + $type_definition = $component['props']['properties'][$prop_or_slot_id]['ui_patterns']['type_definition']; + } + elseif (isset($component['slots'][$prop_or_slot_id])) { + $type_definition = $component['slots'][$prop_or_slot_id]['ui_patterns']['type_definition']; + } + else { + throw new \RuntimeException(sprintf("Prop or Slot '%s' not found in component %s", $prop_or_slot_id, $component["id"])); + } + return $type_definition; + } + + /** + * Get the source plugin for a prop or slot. + * + * @param array $component + * The component definition. + * @param string $prop_or_slot_id + * The prop or slot id. + * @param array $prop_or_slot_configuration + * The prop or slot configuration. + * @param string $source_id + * The source id. + * @param array $context + * The context. + * + * @return \Drupal\ui_patterns\SourcePluginBase|null + * The source plugin or NULL. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + private function getSourcePluginForPropOrSlot(array $component, string $prop_or_slot_id, array $prop_or_slot_configuration, string $source_id, array $context): ?SourcePluginBase { + $type_definition = $this->getComponentPropOrSlotTypeDefinition($component, $prop_or_slot_id); + assertNotNull($type_definition); + $configuration = SourcePluginBase::buildConfiguration($prop_or_slot_id, [ + "ui_patterns" => ["type_definition" => $type_definition], + ], $prop_or_slot_configuration, $context); + $plugin = $this->sourcePluginManager()->createInstance($source_id, $configuration); + return ($plugin instanceof SourcePluginBase) ? $plugin : NULL; + } + + /** + * Runs source plugin test with given test_set and source. + * + * @param array $test_set + * The test set. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function runSourcePluginTest(array $test_set): void { + $component_configuration = $test_set['component']; + $component_id = $component_configuration["component_id"]; + $component = $this->componentManager()->getDefinition($component_id); + $contexts = $this->getContextsByIds($test_set); + $expected_outputs = $test_set['output'] ?? []; + $prop_or_slots = ["props", "slots"]; + foreach ($prop_or_slots as $prop_or_slot) { + if (!isset($expected_outputs[$prop_or_slot], $component_configuration[$prop_or_slot])) { + continue; + } + foreach ($component_configuration[$prop_or_slot] as $prop_or_slot_id => $prop_or_slot_configuration) { + if (isset($expected_outputs[$prop_or_slot][$prop_or_slot_id])) { + // For slots, there is a table of sources for each slot. + $source_to_tests = ($prop_or_slot === "props") ? [$prop_or_slot_configuration] : $prop_or_slot_configuration; + $expected_outputs_here = ($prop_or_slot === "props") ? [$expected_outputs[$prop_or_slot][$prop_or_slot_id]] : $expected_outputs[$prop_or_slot][$prop_or_slot_id]; + foreach ($source_to_tests as $index => $source_to_test) { + if (!isset($source_to_test["source_id"])) { + throw new \RuntimeException(sprintf("Missing source_id for '%s' in test_set '%s'", $prop_or_slot_id, $test_set["name"] ?? "")); + } + $plugin = $this->getSourcePluginForPropOrSlot($component, $prop_or_slot_id, $source_to_test, $source_to_test["source_id"], $contexts); + assertTrue($plugin instanceof SourcePluginBase); + $message = sprintf("Test '%s' failed for prop/slot '%s' of component %s with source '%s' ", $test_set["name"] ?? "", $prop_or_slot_id, $component_id, $source_to_test["source_id"]); + $prop_value = $plugin->getPropValue(); + $this->assertExpectedOutput($expected_outputs_here[$index], $prop_value, $message); + } + } + } + } + } + +} diff --git a/tests/src/Traits/TestContentCreationTrait.php b/tests/src/Traits/TestContentCreationTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..7c5964490e30c3dde48d2457951a409ce2fb3150 --- /dev/null +++ b/tests/src/Traits/TestContentCreationTrait.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Traits; + +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\FieldTypePluginManager; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\FieldConfigInterface; +use Drupal\node\Entity\NodeType; +use Drupal\node\NodeInterface; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Entity test data trait. + */ +trait TestContentCreationTrait { + + use NodeCreationTrait { + createNode as drupalCreateNode; + } + + /** + * Returns the field type plugin manager. + */ + protected function getFieldTypePluginManager(): FieldTypePluginManager { + return \Drupal::service('plugin.manager.field.field_type'); + } + + /** + * Set up test node. + * + * @param string $bundle + * The bundle name. + * @param array $values + * Values applied to create method of the node. + */ + private function createNode(string $bundle = 'page', array $values = []): NodeInterface { + $this->createContentType($bundle); + $node = $this->drupalCreateNode(['type' => $bundle] + $values); + $this->drupalCreateNode(); + return $node; + } + + /** + * Creates content type with fields foreach field type. + * + * The created field names are: + * - field_"type" with cardinality -1 + * - field_"type"_1 with cardinality 1 + */ + protected function createContentType($bundle = 'page'): NodeType { + if ($type = NodeType::load($bundle)) { + return $type; + } + + $type = NodeType::create([ + 'name' => $bundle, + 'type' => $bundle, + ]); + $type->save(); + $field_types = $this->getFieldTypePluginManager()->getDefinitions(); + + foreach (array_keys($field_types) as $field_type_id) { + if ($field_type_id === 'uuid') { + continue; + } + $field_name = sprintf("field_%s_%s", $field_type_id, 1); + $bundle = (string) $type->id(); + $this->createEntityField($type->getEntityType()->getBundleOf(), $bundle, $field_name, $field_type_id, 1); + $field_name = sprintf("field_%s", $field_type_id); + $this->createEntityField($type->getEntityType()->getBundleOf(), $bundle, $field_name, $field_type_id, + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } + return $type; + } + + /** + * Create a field on a content type. + */ + protected function createEntityField( + string $entity_type, + string $bundle, + string $field_name, + string $field_type_id, + int $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ) : FieldConfigInterface { + $field_type_definition = $this->getFieldTypePluginManager()->getDefinition($field_type_id); + // Create a field storage. + $field_storage = [ + 'field_name' => $field_name, + 'entity_type' => $entity_type, + 'type' => $field_type_id, + 'settings' => [], + 'cardinality' => $cardinality, + ]; + FieldStorageConfig::create($field_storage)->save(); + // Create a field instance on the content type. + $field = [ + 'field_name' => $field_storage['field_name'], + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'label' => $field_type_definition['label'], + 'settings' => [], + ]; + $field_config = FieldConfig::create($field); + $field_config->save(); + // Set cardinality. + $field_storage_reload = FieldStorageConfig::loadByName($entity_type, $field_name); + $field_storage_reload->setCardinality($cardinality); + $field_storage_reload->save(); + return $field_config; + } + +} diff --git a/tests/src/Traits/TestDataTrait.php b/tests/src/Traits/TestDataTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..05dee6d987c9e51d682ad941a1a71a9fe0da87c4 --- /dev/null +++ b/tests/src/Traits/TestDataTrait.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Traits; + +use Drupal\Component\Serialization\Yaml; + +/** + * Trait to read fixtures that describe component test situations. + */ +trait TestDataTrait { + + /** + * Loads test dataset fixture. + */ + protected static function loadTestDataFixture($path) { + return new class($path) { + + /** + * The loaded fixture. + * + * @var array + */ + private array $fixture; + + /** + * Constructs the TestDataSet. + * + * @param string $path + * The path to the fixture. + */ + public function __construct(string $path) { + $yaml = file_get_contents($path); + if ($yaml === FALSE) { + throw new \InvalidArgumentException(sprintf("fixture: %s not found.", $path)); + } + $this->fixture = Yaml::decode($yaml); + } + + /** + * Get test data sets. + * + * @return array<string, array<string, mixed> > + * The test data sets. + */ + public function getTestSets() : array { + if (!is_array($this->fixture)) { + return []; + } + $test_sets = $this->fixture; + foreach ($test_sets as $set_name => &$test_set) { + $test_set = array_merge(["name" => $set_name], $test_set); + } + unset($test_set); + return $test_sets; + } + + /** + * Returns data test set from name. + * + * @param string $set_name + * The test set name. + * + * @return array + * The test data set. + */ + public function getTestSet(string $set_name): array { + $test_sets = $this->getTestSets(); + if (is_array($test_sets[$set_name])) { + return array_merge(["name" => $set_name], $test_sets[$set_name]); + } + throw new \Exception(sprintf('Test set "%s" not found.', $set_name)); + } + + }; + } + +} diff --git a/tests/src/fixtures/TestDataSet.yml b/tests/src/fixtures/TestDataSet.yml new file mode 100644 index 0000000000000000000000000000000000000000..989be28a019d1fde332518516a7bbddf52495f63 --- /dev/null +++ b/tests/src/fixtures/TestDataSet.yml @@ -0,0 +1,58 @@ +--- +textfield_default: + component: + component_id: ui_patterns_test:test-component + props: + string: + source_id: textfield + source: + value: 'test input' + output: + props: + string: + value: test input + entity: {} +textfield_slot: + component: + component_id: ui_patterns_test:test-component + slots: + slot: + - + source_id: textfield + source: + value: 'test input' + entity: {} + output: + slots: + slot: + - + value: "test input" +token_default: + component: + component_id: ui_patterns_test:test-component + props: + string: + source_id: token + source: + value: '[node:id]' + entity: {} + output: + props: + string: + regEx: /[0-9]/ + +field_property_default: + component: + component_id: ui_patterns_test:test-component + slots: + slot: + - + source_id: field_property:node:field_text_1:value + entity: + field_text_1: + value: 'value_text_1' + output: + slots: + slot: + - + value: "value_text_1"