Skip to content

Commit 7251aea

Browse files
committed
feat(twig): add test helper
1 parent e7fc5b8 commit 7251aea

File tree

8 files changed

+246
-3
lines changed

8 files changed

+246
-3
lines changed

src/TwigComponent/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- Add new HTML syntax for rendering components: `<twig:ComponentName>`
66
- `true` attribute values now render just the attribute name, `false` excludes it entirely.
7-
7+
- Add helpers for testing components.
88
- The first argument to `AsTwigComponent` is now optional and defaults to the class name.
99

1010
## 2.7.0

src/TwigComponent/doc/index.rst

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

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

src/TwigComponent/src/ComponentFactory.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __construct(
3939
public function metadataFor(string $name): ComponentMetadata
4040
{
4141
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))));
42+
$this->throwUnknownComponentException($name);
4343
}
4444

4545
return new ComponentMetadata($config);
@@ -143,7 +143,7 @@ private function mount(object $component, array &$data): void
143143
private function getComponent(string $name): object
144144
{
145145
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()))));
146+
$this->throwUnknownComponentException($name);
147147
}
148148

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

183183
return $data;
184184
}
185+
186+
/**
187+
* @return never
188+
*/
189+
private function throwUnknownComponentException(string $name): void
190+
{
191+
throw new \InvalidArgumentException(sprintf('Unknown component "%s". The registered components are: %s', $name, implode(', ', array_keys($this->config))));
192+
}
185193
}
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 %%}', $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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
public function __construct(private string $html)
20+
{
21+
}
22+
23+
public function __toString(): string
24+
{
25+
return $this->html;
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
4+
5+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
6+
7+
/**
8+
* @author Kevin Bond <[email protected]>
9+
*/
10+
#[AsTwigComponent]
11+
final class WithSlots
12+
{
13+
}
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>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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\Service\ServiceA;
18+
19+
final class InteractsWithTwigComponentsTest extends KernelTestCase
20+
{
21+
use InteractsWithTwigComponents;
22+
23+
public function testCanMountComponent(): void
24+
{
25+
$component = $this->mountTwigComponent('component_a', [
26+
'propA' => 'prop a value',
27+
'propB' => 'prop b value',
28+
]);
29+
30+
$this->assertInstanceof(ComponentA::class, $component);
31+
$this->assertInstanceOf(ServiceA::class, $component->getService());
32+
$this->assertSame('prop a value', $component->propA);
33+
$this->assertSame('prop b value', $component->getPropB());
34+
}
35+
36+
public function testCanRenderComponent(): void
37+
{
38+
$rendered = $this->renderTwigComponent('component_a', [
39+
'propA' => 'prop a value',
40+
'propB' => 'prop b value',
41+
]);
42+
43+
$this->assertStringContainsString('propA: prop a value', $rendered);
44+
$this->assertStringContainsString('propB: prop b value', $rendered);
45+
$this->assertStringContainsString('service: service a value', $rendered);
46+
}
47+
48+
public function testCanRenderComponentWithSlots(): void
49+
{
50+
$rendered = $this->renderTwigComponent(
51+
name: 'WithSlots',
52+
content: '<p>some content</p>',
53+
blocks: [
54+
'slot1' => '<p>some slot1 content</p>',
55+
'slot2' => $this->renderTwigComponent('component_a', [
56+
'propA' => 'prop a value',
57+
'propB' => 'prop b value',
58+
]),
59+
],
60+
);
61+
62+
$this->assertStringContainsString('<p>some content</p>', $rendered);
63+
$this->assertStringContainsString('<p>some slot1 content</p>', $rendered);
64+
$this->assertStringContainsString('propA: prop a value', $rendered);
65+
$this->assertStringContainsString('propB: prop b value', $rendered);
66+
$this->assertStringContainsString('service: service a value', $rendered);
67+
}
68+
}

0 commit comments

Comments
 (0)