Skip to content

Commit 21d16ea

Browse files
committed
feature #821 [Twig] add test helper (kbond)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Twig] add test helper | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | Fix #818 | License | MIT _This works with Live components out of the box (not with slots though as they aren't supported)._ ```php use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents; class MyComponentTest extends KernelTestCase { use InteractsWithTwigComponents; public function testComponentMount(): void { $component = $this->mountTwigComponent( name: 'MyComponent', // can also use FQCN (MyComponent::class) data: ['foo' => 'bar'], ); $this->assertInstanceOf(MyComponent::class, $component); $this->assertSame('bar', $component->foo); } public function testComponentRenders(): void { $rendered = $this->renderTwigComponent( name: 'MyComponent', // can also use FQCN (MyComponent::class) data: ['foo' => 'bar'], ); $this->assertStringContainsString('bar', $rendered); } public function testEmbeddedComponentRenders(): void { $rendered = $this->renderTwigComponent( name: 'MyComponent', // can also use FQCN (MyComponent::class) data: ['foo' => 'bar'], content: '<div>My content</div>', // "content" (default) block blocks: [ 'header' => '<div>My header</div>', 'menu' => $this->renderTwigComponent('Menu'), // can embed other components ], ); $this->assertStringContainsString('bar', $rendered); } } ``` Commits ------- 0b27ea9 [Twig] add test helper
2 parents 6ee3f52 + 0b27ea9 commit 21d16ea

File tree

10 files changed

+305
-5
lines changed

10 files changed

+305
-5
lines changed

src/TwigComponent/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
- Add new HTML syntax for rendering components: `<twig:ComponentName>`
1212
- `true` attribute values now render just the attribute name, `false` excludes it entirely.
13-
13+
- Add helpers for testing components.
1414
- The first argument to `AsTwigComponent` is now optional and defaults to the class name.
15+
- Allow passing a FQCN to `ComponentFactory` methods.
1516

1617
## 2.7.0
1718

src/TwigComponent/doc/index.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,61 @@ And in your component template you can access your embedded block
922922
{% block footer %}{% endblock %}
923923
</div>
924924

925+
Test Helpers
926+
------------
927+
928+
You can test how your component is mounted and rendered using the
929+
``InteractsWithTwigComponents`` trait::
930+
931+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
932+
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;
933+
934+
class MyComponentTest extends KernelTestCase
935+
{
936+
use InteractsWithTwigComponents;
937+
938+
public function testComponentMount(): void
939+
{
940+
$component = $this->mountTwigComponent(
941+
name: 'MyComponent', // can also use FQCN (MyComponent::class)
942+
data: ['foo' => 'bar'],
943+
);
944+
945+
$this->assertInstanceOf(MyComponent::class, $component);
946+
$this->assertSame('bar', $component->foo);
947+
}
948+
949+
public function testComponentRenders(): void
950+
{
951+
$rendered = $this->renderTwigComponent(
952+
name: 'MyComponent', // can also use FQCN (MyComponent::class)
953+
data: ['foo' => 'bar'],
954+
);
955+
956+
$this->assertStringContainsString('bar', $rendered);
957+
}
958+
959+
public function testEmbeddedComponentRenders(): void
960+
{
961+
$rendered = $this->renderTwigComponent(
962+
name: 'MyComponent', // can also use FQCN (MyComponent::class)
963+
data: ['foo' => 'bar'],
964+
content: '<div>My content</div>', // "content" (default) block
965+
blocks: [
966+
'header' => '<div>My header</div>',
967+
'menu' => $this->renderTwigComponent('Menu'), // can embed other components
968+
],
969+
);
970+
971+
$this->assertStringContainsString('bar', $rendered);
972+
}
973+
}
974+
975+
.. note::
976+
977+
The ``InteractsWithTwigComponents`` trait can only be used in tests that extend
978+
``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``.
979+
925980
Contributing
926981
------------
927982

src/TwigComponent/src/ComponentFactory.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,24 @@
2626
final class ComponentFactory
2727
{
2828
/**
29-
* @param array<string, array> $config
29+
* @param array<string, array> $config
30+
* @param array<class-string, string> $classMap
3031
*/
3132
public function __construct(
3233
private ServiceLocator $components,
3334
private PropertyAccessorInterface $propertyAccessor,
3435
private EventDispatcherInterface $eventDispatcher,
35-
private array $config
36+
private array $config,
37+
private array $classMap,
3638
) {
3739
}
3840

3941
public function metadataFor(string $name): ComponentMetadata
4042
{
43+
$name = $this->classMap[$name] ?? $name;
44+
4145
if (!$config = $this->config[$name] ?? null) {
42-
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->config))));
46+
$this->throwUnknownComponentException($name);
4347
}
4448

4549
return new ComponentMetadata($config);
@@ -142,8 +146,10 @@ private function mount(object $component, array &$data): void
142146

143147
private function getComponent(string $name): object
144148
{
149+
$name = $this->classMap[$name] ?? $name;
150+
145151
if (!$this->components->has($name)) {
146-
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->components->getProvidedServices()))));
152+
$this->throwUnknownComponentException($name);
147153
}
148154

149155
return $this->components->get($name);
@@ -182,4 +188,12 @@ private function postMount(object $component, array $data): array
182188

183189
return $data;
184190
}
191+
192+
/**
193+
* @return never
194+
*/
195+
private function throwUnknownComponentException(string $name): void
196+
{
197+
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->config))));
198+
}
185199
}

src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function process(ContainerBuilder $container): void
2929
$componentConfig = [];
3030

3131
$componentReferences = [];
32+
$componentClassMap = [];
3233
$componentNames = [];
3334
foreach ($container->findTaggedServiceIds('twig.component') as $id => $tags) {
3435
$definition = $container->findDefinition($id);
@@ -52,11 +53,13 @@ public function process(ContainerBuilder $container): void
5253
$componentConfig[$tag['key']] = $tag;
5354
$componentReferences[$tag['key']] = new Reference($id);
5455
$componentNames[] = $tag['key'];
56+
$componentClassMap[$tag['class']] = $tag['key'];
5557
}
5658
}
5759

5860
$factoryDefinition = $container->findDefinition('ux.twig_component.component_factory');
5961
$factoryDefinition->setArgument(0, ServiceLocatorTagPass::register($container, $componentReferences));
6062
$factoryDefinition->setArgument(3, $componentConfig);
63+
$factoryDefinition->setArgument(4, $componentClassMap);
6164
}
6265
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Test;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
16+
/**
17+
* @author Kevin Bond <[email protected]>
18+
*/
19+
trait InteractsWithTwigComponents
20+
{
21+
protected function mountTwigComponent(string $name, array $data = []): object
22+
{
23+
if (!$this instanceof KernelTestCase) {
24+
throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
25+
}
26+
27+
return static::getContainer()->get('ux.twig_component.component_factory')->create($name, $data)->getComponent();
28+
}
29+
30+
/**
31+
* @param array<string,string> $blocks
32+
*/
33+
protected function renderTwigComponent(string $name, array $data = [], ?string $content = null, array $blocks = []): RenderedComponent
34+
{
35+
if (!$this instanceof KernelTestCase) {
36+
throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
37+
}
38+
39+
$blocks = array_filter(array_merge($blocks, ['content' => $content]));
40+
41+
if (!$blocks) {
42+
return new RenderedComponent(self::getContainer()->get('twig')
43+
->createTemplate('{{ component(name, data) }}')
44+
->render([
45+
'name' => $name,
46+
'data' => $data,
47+
])
48+
);
49+
}
50+
51+
$template = sprintf('{%% component "%s" with data %%}', addslashes($name));
52+
53+
foreach (array_keys($blocks) as $blockName) {
54+
$template .= sprintf('{%% block %1$s %%}{{ blocks.%1$s|raw }}{%% endblock %%}', $blockName);
55+
}
56+
57+
$template .= '{% endcomponent %}';
58+
59+
return new RenderedComponent(self::getContainer()->get('twig')
60+
->createTemplate($template)
61+
->render([
62+
'data' => $data,
63+
'blocks' => $blocks,
64+
])
65+
);
66+
}
67+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Test;
13+
14+
/**
15+
* @author Kevin Bond <[email protected]>
16+
*/
17+
final class RenderedComponent implements \Stringable
18+
{
19+
/**
20+
* @internal
21+
*/
22+
public function __construct(private string $html)
23+
{
24+
}
25+
26+
public function __toString(): string
27+
{
28+
return $this->html;
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @author Kevin Bond <[email protected]>
18+
*/
19+
#[AsTwigComponent]
20+
final class WithSlots
21+
{
22+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
{% block content %}{% endblock %}
3+
{% block slot1 %}{% endblock %}
4+
{% block slot2 %}{% endblock %}
5+
</div>

src/TwigComponent/tests/Integration/ComponentFactoryTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentA;
1818
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentB;
1919
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentC;
20+
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\WithSlots;
2021

2122
/**
2223
* @author Kevin Bond <[email protected]>
@@ -170,6 +171,18 @@ public function testInputPropsStoredOnMountedComponent(): void
170171
$this->assertSame(['propA' => 'A', 'propB' => 'B'], $mountedComponent->getInputProps());
171172
}
172173

174+
/**
175+
* @doesNotPerformAssertions
176+
*/
177+
public function testGetComponentWithClassName(): void
178+
{
179+
$factory = $this->factory();
180+
181+
$factory->create(WithSlots::class);
182+
$factory->get(WithSlots::class);
183+
$factory->metadataFor(WithSlots::class);
184+
}
185+
173186
private function factory(): ComponentFactory
174187
{
175188
return self::getContainer()->get('ux.twig_component.component_factory');
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Tests\Integration\Test;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;
16+
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentA;
17+
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\WithSlots;
18+
use Symfony\UX\TwigComponent\Tests\Fixtures\Service\ServiceA;
19+
20+
final class InteractsWithTwigComponentsTest extends KernelTestCase
21+
{
22+
use InteractsWithTwigComponents;
23+
24+
/**
25+
* @dataProvider componentANameProvider
26+
*/
27+
public function testCanMountComponent(string $name): void
28+
{
29+
$component = $this->mountTwigComponent($name, [
30+
'propA' => 'prop a value',
31+
'propB' => 'prop b value',
32+
]);
33+
34+
$this->assertInstanceof(ComponentA::class, $component);
35+
$this->assertInstanceOf(ServiceA::class, $component->getService());
36+
$this->assertSame('prop a value', $component->propA);
37+
$this->assertSame('prop b value', $component->getPropB());
38+
}
39+
40+
/**
41+
* @dataProvider componentANameProvider
42+
*/
43+
public function testCanRenderComponent(string $name): void
44+
{
45+
$rendered = $this->renderTwigComponent($name, [
46+
'propA' => 'prop a value',
47+
'propB' => 'prop b value',
48+
]);
49+
50+
$this->assertStringContainsString('propA: prop a value', $rendered);
51+
$this->assertStringContainsString('propB: prop b value', $rendered);
52+
$this->assertStringContainsString('service: service a value', $rendered);
53+
}
54+
55+
/**
56+
* @dataProvider withSlotsNameProvider
57+
*/
58+
public function testCanRenderComponentWithSlots(string $name): void
59+
{
60+
$rendered = $this->renderTwigComponent(
61+
name: $name,
62+
content: '<p>some content</p>',
63+
blocks: [
64+
'slot1' => '<p>some slot1 content</p>',
65+
'slot2' => $this->renderTwigComponent('component_a', [
66+
'propA' => 'prop a value',
67+
'propB' => 'prop b value',
68+
]),
69+
],
70+
);
71+
72+
$this->assertStringContainsString('<p>some content</p>', $rendered);
73+
$this->assertStringContainsString('<p>some slot1 content</p>', $rendered);
74+
$this->assertStringContainsString('propA: prop a value', $rendered);
75+
$this->assertStringContainsString('propB: prop b value', $rendered);
76+
$this->assertStringContainsString('service: service a value', $rendered);
77+
}
78+
79+
public static function componentANameProvider(): iterable
80+
{
81+
yield ['component_a'];
82+
yield [ComponentA::class];
83+
}
84+
85+
public static function withSlotsNameProvider(): iterable
86+
{
87+
yield ['WithSlots'];
88+
yield [WithSlots::class];
89+
}
90+
}

0 commit comments

Comments
 (0)