diff --git a/src/Element/ComponentElementAlter.php b/src/Element/ComponentElementAlter.php index f4e944e77e7b94a4c5de7e61863c4e3c0dc98752..13a8c58bbbb7d556c96e4335cf2fe6be28b600ee 100644 --- a/src/Element/ComponentElementAlter.php +++ b/src/Element/ComponentElementAlter.php @@ -5,11 +5,9 @@ declare(strict_types=1); namespace Drupal\ui_patterns\Element; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Plugin\Component; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\ComponentPluginManager; use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; -use Drupal\ui_patterns\PropTypeAdapterPluginManager; /** * Our additions to the SDC render element. @@ -19,27 +17,25 @@ class ComponentElementAlter implements TrustedCallbackInterface { /** * Constructs a ComponentElementAlter. */ - public function __construct(protected ComponentPluginManager $componentPluginManager, protected PropTypeAdapterPluginManager $adaptersManager) { - } + public function __construct(protected ComponentPluginManager $componentPluginManager) {} /** * {@inheritdoc} */ public static function trustedCallbacks() { return ['alter']; + } /** * Alter SDC component element. + * + * The ::normalizeProps() methods logic has been moved to + * TwigExtension::normalizeProps() in order to be executed also when + * components are loaded from Twig include or embed. */ public function alter(array $element): array { $element = $this->normalizeSlots($element); - $component = $this->componentPluginManager->find($element['#component']); - $element = $this->normalizeProps($element, $component); - // Attributes prop must never be empty, to avoid the processing of SDC's - // ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding - // an Attribute PHP object before running the validator. - $element["#props"]["attributes"]['data-component-id'] = $component->getPluginId(); $element = $this->processAttributesRenderProperty($element); return $element; } @@ -64,32 +60,6 @@ class ComponentElementAlter implements TrustedCallbackInterface { return $element; } - /** - * Normalize props. - */ - public function normalizeProps(array $element, Component $component): array { - $props = $component->metadata->schema['properties'] ?? []; - foreach ($element["#props"] as $prop_id => $prop) { - if (!isset($props[$prop_id])) { - continue; - } - $definition = $props[$prop_id]; - $prop_type = $definition['ui_patterns']['type_definition']; - // Normalizing attributes to an array is not working - // if the prop type is defined by type=Drupal\Core\Template\Attribute - // This should actually be done by the normalize function. - $data = $prop_type->normalize($prop); - if (isset($definition['ui_patterns']['prop_type_adapter'])) { - $prop_type_adapter_id = $definition['ui_patterns']['prop_type_adapter']; - /** @var \Drupal\ui_patterns\PropTypeAdapterInterface $prop_type_adapter */ - $prop_type_adapter = $this->adaptersManager->createInstance($prop_type_adapter_id); - $data = $prop_type_adapter->transform($data); - } - $element["#props"][$prop_id] = $data; - } - return $element; - } - /** * Process #attributes render property. * diff --git a/src/Template/ComponentNodeVisitor.php b/src/Template/ComponentNodeVisitor.php deleted file mode 100755 index 6da6ff0e1fb1b73bf360a953b7f265ecd8be4432..0000000000000000000000000000000000000000 --- a/src/Template/ComponentNodeVisitor.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php - -namespace Drupal\ui_patterns\Template; - -use Drupal\Core\Plugin\Component; -use Drupal\Core\Render\Component\Exception\ComponentNotFoundException; -use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor; -use Drupal\Core\Theme\ComponentPluginManager; -use Twig\Environment; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\FunctionExpression; -use Twig\Node\ModuleNode; -use Twig\Node\Node; -use Twig\Node\PrintNode; -use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\TwigFunction; - -/** - * Provides a ComponentNodeVisitor to change the generated parse-tree. - * - * @internal - */ -class ComponentNodeVisitor implements NodeVisitorInterface { - - /** - * Node name: expr. - */ - const NODE_NAME_EXPR = 'expr'; - - /** - * The component plugin manager. - */ - protected ComponentPluginManager $componentManager; - - /** - * Constructs a new ComponentNodeVisitor object. - * - * @param \Drupal\Core\Theme\ComponentPluginManager $component_plugin_manager - * The component plugin manager. - */ - public function __construct(ComponentPluginManager $component_plugin_manager) { - $this->componentManager = $component_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public function enterNode(Node $node, Environment $env): Node { - return $node; - } - - /** - * {@inheritdoc} - */ - public function leaveNode(Node $node, Environment $env): ?Node { - if (!$node instanceof ModuleNode) { - return $node; - } - $component = $this->getComponent($node); - if (!($component instanceof Component)) { - return $node; - } - $line = $node->getTemplateLine(); - $function = $this->buildPreprocessPropsFunction($line, $component, $env); - $node = $this->injectFunction($node, $function); - return $node; - } - - /** - * {@inheritdoc} - */ - public function getPriority(): int { - $priority = &drupal_static(__METHOD__); - if (!isset($priority)) { - $original_node_visitor = new CoreComponentNodeVisitor($this->componentManager); - // Ensure that this component node visitor's priority is higher than - // core's node visitor class for components, because this class has to run - // core's class. - $priority = $original_node_visitor->getPriority() + 1; - } - return is_numeric($priority) ? (int) $priority : 0; - } - - /** - * Finds the SDC for the current module node. - * - * A duplicate of \Drupal\Core\Template\ComponentNodeVisitor::getComponent() - * - * @param \Twig\Node\Node $node - * The node. - * - * @return \Drupal\Core\Plugin\Component|null - * The component, if any. - */ - protected function getComponent(Node $node): ?Component { - $component_id = $node->getTemplateName(); - if (!preg_match('/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/', $component_id)) { - return NULL; - } - try { - return $this->componentManager->find($component_id); - } - catch (ComponentNotFoundException $e) { - return NULL; - } - } - - /** - * Build the _ui_patterns_preprocess_props Twig function. - * - * @param int $line - * The line . - * @param \Drupal\Core\Plugin\Component $component - * The component. - * @param \Twig\Environment $env - * A Twig Environment instance. - * - * @return \Twig\Node\Node - * The Twig function. - */ - protected function buildPreprocessPropsFunction(int $line, Component $component, Environment $env): Node { - $component_id = $component->getPluginId(); - $function_parameter = new ConstantExpression($component_id, $line); - $function_parameters_node = new Node([$function_parameter]); - $function = new FunctionExpression( - new TwigFunction('_ui_patterns_preprocess_props', [$env->getExtension(TwigExtension::class), 'preprocessProps'], ['needs_context' => TRUE]), - $function_parameters_node, - $line - ); - return new PrintNode($function, $line); - } - - /** - * Injects custom Twig nodes into given node as child nodes. - * - * The function will be injected direct after validate_component_props - * function already injected by SDC's ComponentNodeVisitor. - * - * @param \Twig\Node\Node $node - * The node where we will inject the function in. - * @param \Twig\Node\Node $function - * The Twig function. - * - * @return \Twig\Node\Node - * The node with the function inserted. - */ - protected function injectFunction(Node $node, Node $function): Node { - $insertion = new Node([$node->getNode('display_start'), $function]); - $node->setNode('display_start', $insertion); - return $node; - } - -} diff --git a/src/Template/ModuleNodeVisitorAfterSdc.php b/src/Template/ModuleNodeVisitorAfterSdc.php new file mode 100755 index 0000000000000000000000000000000000000000..17e7ebfd6ffb32a1a6377e7bf645e105515ab740 --- /dev/null +++ b/src/Template/ModuleNodeVisitorAfterSdc.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\ui_patterns\Template; + +use Drupal\Core\Plugin\Component; +use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor; +use Twig\Environment; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\FunctionExpression; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\Node\PrintNode; +use Twig\TwigFunction; + +/** + * Provides a Node Visitor to change the generated parse-tree. + */ +class ModuleNodeVisitorAfterSdc extends ModuleNodeVisitorBase { + + /** + * {@inheritdoc} + */ + public function leaveNode(Node $node, Environment $env): ?Node { + if (!$node instanceof ModuleNode) { + return $node; + } + $component = $this->getComponent($node); + if (!($component instanceof Component)) { + return $node; + } + $line = $node->getTemplateLine(); + $function = $this->buildPreprocessPropsFunction($line, $component, $env); + $node = $this->injectFunction($node, $function); + return $node; + } + + /** + * {@inheritdoc} + */ + public function getPriority(): int { + $priority = &drupal_static(__METHOD__); + if (!isset($priority)) { + $original_node_visitor = new CoreComponentNodeVisitor($this->componentManager); + // Ensure that this node visitor's priority is higher than core's visitor, + // because this class has to run after core's class. + $priority = $original_node_visitor->getPriority() + 1; + } + return is_numeric($priority) ? (int) $priority : 0; + } + + /** + * Build the _ui_patterns_preprocess_props Twig function. + * + * @param int $line + * The line. + * @param \Drupal\Core\Plugin\Component $component + * The component. + * @param \Twig\Environment $env + * A Twig Environment instance. + * + * @return \Twig\Node\Node + * The Twig function. + */ + protected function buildPreprocessPropsFunction(int $line, Component $component, Environment $env): Node { + $component_id = $component->getPluginId(); + $function_parameter = new ConstantExpression($component_id, $line); + $function_parameters_node = new Node([$function_parameter]); + $function = new FunctionExpression( + new TwigFunction('_ui_patterns_preprocess_props', [$env->getExtension(TwigExtension::class), 'preprocessProps'], ['needs_context' => TRUE]), + $function_parameters_node, + $line + ); + return new PrintNode($function, $line); + } + +} diff --git a/src/Template/ModuleNodeVisitorBase.php b/src/Template/ModuleNodeVisitorBase.php new file mode 100755 index 0000000000000000000000000000000000000000..d00f757bbb9b3e883953db98d950c98b6f74952f --- /dev/null +++ b/src/Template/ModuleNodeVisitorBase.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\ui_patterns\Template; + +use Drupal\Core\Plugin\Component; +use Drupal\Core\Render\Component\Exception\ComponentNotFoundException; +use Drupal\Core\Theme\ComponentPluginManager; +use Twig\Environment; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +/** + * Provides a Node Visitor to change the generated parse-tree. + */ +abstract class ModuleNodeVisitorBase implements NodeVisitorInterface { + + /** + * The component plugin manager. + */ + protected ComponentPluginManager $componentManager; + + /** + * Constructs a new ComponentNodeVisitor object. + * + * @param \Drupal\Core\Theme\ComponentPluginManager $component_plugin_manager + * The component plugin manager. + */ + public function __construct(ComponentPluginManager $component_plugin_manager) { + $this->componentManager = $component_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function enterNode(Node $node, Environment $env): Node { + return $node; + } + + /** + * Finds the SDC for the current module node. + * + * A duplicate of \Drupal\Core\Template\ComponentNodeVisitor::getComponent() + * + * @param \Twig\Node\Node $node + * The node. + * + * @return \Drupal\Core\Plugin\Component|null + * The component, if any. + */ + protected function getComponent(Node $node): ?Component { + $component_id = $node->getTemplateName(); + if (!preg_match('/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/', $component_id)) { + return NULL; + } + try { + return $this->componentManager->find($component_id); + } + catch (ComponentNotFoundException $e) { + return NULL; + } + } + + /** + * Injects custom Twig nodes into given node as child nodes. + * + * The function will be injected direct after validate_component_props + * function already injected by SDC's ComponentNodeVisitor. + * + * @param \Twig\Node\Node $node + * The node where we will inject the function in. + * @param \Twig\Node\Node $function + * The Twig function. + * + * @return \Twig\Node\Node + * The node with the function inserted. + */ + protected function injectFunction(Node $node, Node $function): Node { + $insertion = new Node([$node->getNode('display_start'), $function]); + $node->setNode('display_start', $insertion); + return $node; + } + +} diff --git a/src/Template/ModuleNodeVisitorBeforeSdc.php b/src/Template/ModuleNodeVisitorBeforeSdc.php new file mode 100755 index 0000000000000000000000000000000000000000..9c5e3a1e6c1bd348a7d7a38c66dccca62dd04fc4 --- /dev/null +++ b/src/Template/ModuleNodeVisitorBeforeSdc.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\ui_patterns\Template; + +use Drupal\Core\Plugin\Component; +use Drupal\Core\Template\ComponentNodeVisitor as CoreComponentNodeVisitor; +use Twig\Environment; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\FunctionExpression; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\Node\PrintNode; +use Twig\TwigFunction; + +/** + * Provides a Node Visitor to change the generated parse-tree. + */ +class ModuleNodeVisitorBeforeSdc extends ModuleNodeVisitorBase { + + /** + * {@inheritdoc} + */ + public function leaveNode(Node $node, Environment $env): ?Node { + if (!$node instanceof ModuleNode) { + return $node; + } + $component = $this->getComponent($node); + if (!($component instanceof Component)) { + return $node; + } + $line = $node->getTemplateLine(); + $function = $this->buildNormalizePropsFunction($line, $component, $env); + $node = $this->injectFunction($node, $function); + return $node; + } + + /** + * {@inheritdoc} + */ + public function getPriority(): int { + $priority = &drupal_static(__METHOD__); + if (!isset($priority)) { + $original_node_visitor = new CoreComponentNodeVisitor($this->componentManager); + // Ensure that this node visitor's priority is lower than core's visitor, + // because this class has to run before core's class. + $priority = $original_node_visitor->getPriority() - 1; + } + return is_numeric($priority) ? (int) $priority : 0; + } + + /** + * Build the _ui_patterns_preprocess_props Twig function. + * + * @param int $line + * The line. + * @param \Drupal\Core\Plugin\Component $component + * The component. + * @param \Twig\Environment $env + * A Twig Environment instance. + * + * @return \Twig\Node\Node + * The Twig function. + */ + protected function buildNormalizePropsFunction(int $line, Component $component, Environment $env): Node { + $component_id = $component->getPluginId(); + $function_parameter = new ConstantExpression($component_id, $line); + $function_parameters_node = new Node([$function_parameter]); + $function = new FunctionExpression( + new TwigFunction('_ui_patterns_normalize_props', [$env->getExtension(TwigExtension::class), 'normalizeProps'], ['needs_context' => TRUE]), + $function_parameters_node, + $line + ); + return new PrintNode($function, $line); + } + +} diff --git a/src/Template/TwigExtension.php b/src/Template/TwigExtension.php index d193e8ab09c636b7e2664a4e18a4764849023ffd..7595e4b465a2aed4093771fc3adbb1686f157e82 100644 --- a/src/Template/TwigExtension.php +++ b/src/Template/TwigExtension.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace Drupal\ui_patterns\Template; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\ui_patterns\ComponentPluginManager; +use Drupal\ui_patterns\Plugin\UiPatterns\PropType\SlotPropType; +use Drupal\ui_patterns\PropTypeAdapterPluginManager; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -23,9 +26,15 @@ class TwigExtension extends AbstractExtension { * * @param \Drupal\ui_patterns\ComponentPluginManager $componentManager * The component plugin manager. + * @param \Drupal\ui_patterns\PropTypeAdapterPluginManager $adapterManager + * The prop type adapter plugin manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ public function __construct( protected ComponentPluginManager $componentManager, + protected PropTypeAdapterPluginManager $adapterManager, + protected MessengerInterface $messenger, ) {} /** @@ -40,7 +49,8 @@ class TwigExtension extends AbstractExtension { */ public function getNodeVisitors(): array { return [ - new ComponentNodeVisitor($this->componentManager), + new ModuleNodeVisitorBeforeSdc($this->componentManager), + new ModuleNodeVisitorAfterSdc($this->componentManager), ]; } @@ -49,7 +59,11 @@ class TwigExtension extends AbstractExtension { */ public function getFunctions() { return [ + // @todo Remove component() before 2.0.0 release. new TwigFunction('component', [$this, 'renderComponent']), + // For ComponentNodeVisitorBeforeSdc. + new TwigFunction('_ui_patterns_normalize_props', [$this, 'normalizeProps'], ['needs_context' => TRUE]), + // For ComponentNodeVisitorAfterSdc. new TwigFunction('_ui_patterns_preprocess_props', [$this, 'preprocessProps'], ['needs_context' => TRUE]), ]; } @@ -80,6 +94,7 @@ class TwigExtension extends AbstractExtension { * @see \Drupal\Core\Theme\Element\ComponentElement */ public function renderComponent(string $component_id, array $slots = [], array $props = []) { + $this->messenger->addWarning("component() Twig function is deprecated in favor of include() function and will be removed before 2.0.0."); return [ '#type' => 'component', '#component' => $component_id, @@ -88,6 +103,51 @@ class TwigExtension extends AbstractExtension { ]; } + /** + * Normalize props (and slots). + * + * This function must not be used by the templates authors. In a perfect + * world, it would not be necessary to set such a function. We did that to be + * compatible with SDC's ComponentNodeVisitor, in order to execute props + * normalization before SDC's validate_component_props Twig function. + * + * See ModuleNodeVisitorBeforeSdc. + * + * @param array $context + * The context provided to the component. + * @param string $component_id + * The component ID. + * + * @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException + */ + public function normalizeProps(array &$context, string $component_id): void { + $component = $this->componentManager->find($component_id); + $props = $component->metadata->schema['properties'] ?? []; + // Attributes prop must never be empty, to avoid the processing of SDC's + // ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding + // an Attribute PHP object before running the validator. + // Attribute PHP object are casted as string by the validator and trigger + // '[attributes] String value found, but an object is required' error. + $context['attributes']['data-component-id'] = $component->getPluginId(); + foreach ($context as $variable => $value) { + if (isset($component->metadata->slots[$variable])) { + $context[$variable] = SlotPropType::normalize($value); + continue; + } + if (!isset($props[$variable])) { + continue; + } + $prop_type = $props[$variable]['ui_patterns']['type_definition']; + $context[$variable] = $prop_type->normalize($value); + if (isset($props[$variable]['ui_patterns']['prop_type_adapter'])) { + $prop_type_adapter_id = $props[$variable]['ui_patterns']['prop_type_adapter']; + /** @var \Drupal\ui_patterns\PropTypeAdapterInterface $prop_type_adapter */ + $prop_type_adapter = $this->adapterManager->createInstance($prop_type_adapter_id); + $context[$variable] = $prop_type_adapter->transform($context[$variable]); + } + } + } + /** * Preprocess props. * @@ -96,6 +156,8 @@ class TwigExtension extends AbstractExtension { * compatible with SDC's ComponentNodeVisitor, in order to execute props * preprocessing after SDC's validate_component_props Twig function. * + * See ModuleNodeVisitorAfterSdc. + * * @param array $context * The context provided to the component. * @param string $component_id diff --git a/tests/modules/ui_patterns_test/components/alert/alert.twig b/tests/modules/ui_patterns_test/components/alert/alert.twig index 04952e93a93d61ce32b1105071b2d5b8aac006b3..15212aeec969d0ae9d001dea651dae803401adaf 100644 --- a/tests/modules/ui_patterns_test/components/alert/alert.twig +++ b/tests/modules/ui_patterns_test/components/alert/alert.twig @@ -12,10 +12,10 @@ {% endif %} {{ message|add_class('alert-link') }} {% if dismissible %} - {{ component('ui_patterns_test:close_button', {}, { - attributes: create_attribute({ - 'data-bs-dismiss': 'alert' - }), + {{ include('ui_patterns_test:close_button', { + attributes: { + 'data-bs-dismiss': 'alert', + }, aria_label: 'Close'|t, }) }} {% endif %} 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 index 63caaecf27f4fabfff53682d4991cd5bd8ad4025..847fc7439e99d9c7378e8e88087fd76995bcb865 100644 --- a/tests/modules/ui_patterns_test/components/test-component/test-component.twig +++ b/tests/modules/ui_patterns_test/components/test-component/test-component.twig @@ -1,4 +1,4 @@ -<div class="ui-patterns-test-component"> +<div{{ attributes.addClass(["ui-patterns-test-component"]) }}> <div class="ui-patterns-props-string"> {{ string }} </div> @@ -57,13 +57,13 @@ {% endfor %} </div> <div class="ui-patterns-props-attributes_implicit"> - {{ attributes_implicit }} + <div{{ attributes_implicit }}></div> </div> <div class="ui-patterns-props-attributes_ui_patterns"> - {{ attributes_ui_patterns }} + <div{{ attributes_ui_patterns }}></div> </div> <div class="ui-patterns-props-attributes_class"> - {{ attributes_class }} + <div{{ attributes_class }}></div> </div> <div class="ui-patterns-slots-slot"> {{ slot }} diff --git a/tests/src/Kernel/TwigVisitorTest.php b/tests/src/Kernel/TwigVisitorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e4738078e67bbe5bea700f27228e0d488ad805b0 --- /dev/null +++ b/tests/src/Kernel/TwigVisitorTest.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ui_patterns\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\ui_patterns\Traits\TestDataTrait; + +/** + * Base class to test source plugins. + * + * @group ui_patterns + */ +class TwigVisitorTest extends KernelTestBase { + + use TestDataTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'system', + 'user', + 'text', + 'field', + 'node', + 'ui_patterns', + 'ui_patterns_test', + 'datetime', + 'filter', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installSchema('node', 'node_access'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig(['system', 'filter']); + } + + /** + * Test attributes. + */ + public function testAttributes() : void { + + $default_context = [ + 'prop_string' => $this->randomMachineName(), + 'attributes' => [], + ]; + $twig_templates = [ + // Include tag. + '{% include "ui_patterns_test:test-component" with { string: prop_string } %}', + '{% include "ui_patterns_test:test-component" with { attributes: {}, string: prop_string } %}', + '{% include "ui_patterns_test:test-component" with { attributes: create_attribute(), string: prop_string } %}', + '{% include "ui_patterns_test:test-component" with { attributes: "", string: prop_string } %}', + // Embed tag. + "{% embed 'ui_patterns_test:test-component' with { attributes: {}, string: prop_string } only %} + {% block content %} + {{ content }} + {% endblock %} + {% endembed %}", + "{% embed 'ui_patterns_test:test-component' with { attributes: create_attribute(), string: prop_string } only %} + {% block content %} + {{ content }} + {% endblock %} + {% endembed %}", + "{% embed 'ui_patterns_test:test-component' with { attributes: '', string: prop_string } only %} + {% block content %} + {{ content }} + {% endblock %} + {% endembed %}", + "{% embed 'ui_patterns_test:test-component' with { string: prop_string } only %} + {% block content %} + {{ content }} + {% endblock %} + {% endembed %}", + // Include function. + '{{ include("ui_patterns_test:test-component", { string: prop_string }) }}', + '{{ include("ui_patterns_test:test-component", { attributes: {}, string: prop_string }) }}', + '{{ include("ui_patterns_test:test-component", { attributes: create_attribute(), string: prop_string }) }}', + '{{ include("ui_patterns_test:test-component", { attributes: "", string: prop_string }) }}', + ]; + $render_array = [ + '#type' => 'inline_template', + '#context' => $default_context, + ]; + foreach ($twig_templates as $twig_template) { + $render_array_test = array_merge($render_array, ['#template' => $twig_template]); + $this->assertExpectedOutput( + [ + "rendered_value_plain" => $default_context["prop_string"], + "rendered_value" => $default_context["prop_string"], + "assert" => "assertStringContainsString", + ], + $render_array_test + ); + } + } + +} diff --git a/tests/src/Traits/TestDataTrait.php b/tests/src/Traits/TestDataTrait.php index 61d2c856f146e2f7255e9733bcb349667d67dab2..693e50b60e9d4f76f5b55f22aea2158f0a2fabc0 100644 --- a/tests/src/Traits/TestDataTrait.php +++ b/tests/src/Traits/TestDataTrait.php @@ -54,7 +54,7 @@ trait TestDataTrait { * * @param array $expected_result * The expected result from the test set. - * @param string $result + * @param mixed $result * The result. * @param string $message * The message. @@ -88,17 +88,26 @@ trait TestDataTrait { } if (isset($expected_result['rendered_value']) || isset($expected_result['rendered_value_plain'])) { // $rendered = \Drupal::service('renderer')->renderRoot($result); - $rendered = is_array($result) ? \Drupal::service('renderer')->renderRoot($result) : $result; + $rendered = NULL; + try { + $rendered = is_array($result) ? \Drupal::service('renderer')->renderRoot($result) : $result; + } + catch (\Exception $e) { + // @phpstan-ignore-next-line + $this->assertTrue(FALSE, sprintf("%s: ERROR, failed to render result: %s \n (%s)", $message, $e->getMessage(), print_r($result, TRUE))); + } if ($rendered instanceof MarkupInterface) { $rendered = "" . $rendered; } $normalized_rendered = self::normalizeMarkupString($rendered); if (isset($expected_result['rendered_value'])) { - $this->assertContains($expected_result['rendered_value'], [$normalized_rendered], sprintf("%s: '%s' VS '%s' (%s)", $message, $expected_result['rendered_value'], $normalized_rendered, print_r($result, TRUE))); + $message = sprintf("%s: '%s' VS '%s' (%s)", $message, $expected_result['rendered_value'], $normalized_rendered, print_r($result, TRUE)); + $this->assertExpectedOutputGeneric($expected_result['rendered_value'], $normalized_rendered, $expected_result, $message); } if (isset($expected_result['rendered_value_plain'])) { $rendered_plain = Xss::filter($normalized_rendered); - $this->assertContains($expected_result['rendered_value_plain'], [$rendered_plain], sprintf("%s: '%s' VS '%s'", $message, $rendered_plain, $normalized_rendered)); + $message = sprintf("%s: '%s' VS '%s'", $message, $rendered_plain, $normalized_rendered); + $this->assertExpectedOutputGeneric($expected_result['rendered_value_plain'], $rendered_plain, $expected_result, $message); } $assert_done = TRUE; } @@ -111,6 +120,29 @@ trait TestDataTrait { } } + /** + * Assert expected output generic. + * + * @param mixed $expected_argument + * The expected argument. + * @param mixed $computed_data + * The computed data. + * @param array $expected_result_metadata + * The expected result metadata. + * @param string $message + * The message. + */ + protected function assertExpectedOutputGeneric(mixed $expected_argument, mixed $computed_data, array $expected_result_metadata, string $message = ''): void { + $haystack = $computed_data; + if (!isset($expected_result_metadata["assert"])) { + $expected_result_metadata["assert"] = "assertContains"; + } + if ($expected_result_metadata["assert"] === "assertContains") { + $haystack = [$computed_data]; + } + $this->{$expected_result_metadata["assert"]}($expected_argument, $haystack, $message); + } + /** * Normalize a string of markup for comparison. */ diff --git a/ui_patterns.services.yml b/ui_patterns.services.yml index abffbf352b4d9e402649948f7be24826407bb364..593f10246a27be5dcc66fbb7aab491a982d0e57e 100644 --- a/ui_patterns.services.yml +++ b/ui_patterns.services.yml @@ -75,6 +75,8 @@ services: - { name: twig.extension } arguments: - "@plugin.manager.sdc" + - "@plugin.manager.ui_patterns_prop_type_adapter" + - '@messenger' ui_patterns.sample_entity_generator: class: Drupal\ui_patterns\Entity\SampleEntityGenerator