Skip to content

Commit 25f0ab8

Browse files
committed
feature #823 [Live] add test helper (kbond)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Live] add test helper | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | Continuation of #821 | License | MIT ```php use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents; class MyComponentTest extends KernelTestCase { use InteractsWithLiveComponents; public function testThisComponent(): void { $testComponent = $this->createLiveComponent('my_component'); $testComponent = $this->createLiveComponent(Component::class); // or use the FQCN (string) $testComponent->render(); // string - initial state html $testComponent->component(); // object - component instance at initial state $testComponent ->call('increase') // call a live action ->call('increase', ['amount' => 2]) // call a live action with arguments ->set('count', 5) // set a live prop ->emit('inceaseEvent') // emit a live event ->emit('inceaseEvent', ['amount' => 2]) // emit a live event with arguments ->refresh() // simply refresh the component ; (string) $testComponent->render(); // string - updated state html $testComponent->component(); // object - component instance at updated state /** `@var` Symfony\Component\HttpFoundation\Response $response */ $response = $testComponent->call('actionThatRedirects')->response(); } } ``` TODO: - [x] handle action responses - [x] `->refresh()` method? - [x] Docs - [x] Changelog Commits ------- faa1518 [Live] add test helper
2 parents da3c2ff + faa1518 commit 25f0ab8

File tree

13 files changed

+442
-0
lines changed

13 files changed

+442
-0
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.11.0
4+
5+
- Add helper for testing live components.
6+
37
## 2.9.0
48

59
- Add support for symfony/asset-mapper

src/LiveComponent/doc/index.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,6 +3033,74 @@ Then specify this new route on your component:
30333033
use DefaultActionTrait;
30343034
}
30353035
3036+
Test Helper
3037+
-----------
3038+
3039+
.. versionadded:: 2.11
3040+
3041+
The test helper was added in LiveComponents 2.11.
3042+
3043+
For testing, you can use the ``InteractsWithLiveComponents`` trait which
3044+
uses Symfony's test client to render and make requests to your components::
3045+
3046+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
3047+
use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
3048+
3049+
class MyComponentTest extends KernelTestCase
3050+
{
3051+
use InteractsWithLiveComponents;
3052+
3053+
public function testCanRenderAndInteract(): void
3054+
{
3055+
$testComponent = $this->createLiveComponent(
3056+
name: 'MyComponent', // can also use FQCN (MyComponent::class)
3057+
data: ['foo' => 'bar'],
3058+
);
3059+
3060+
// render the component html
3061+
$this->assertStringContainsString('Count: 0', $testComponent->render());
3062+
3063+
// call live actions
3064+
$testComponent
3065+
->call('increase')
3066+
->call('increase', ['amount' => 2]) // call a live action with arguments
3067+
;
3068+
3069+
$this->assertStringContainsString('Count: 3', $testComponent->render());
3070+
3071+
// emit live events
3072+
$testComponent
3073+
->emit('increaseEvent')
3074+
->emit('increaseEvent', ['amount' => 2]) // emit a live event with arguments
3075+
;
3076+
3077+
// set live props
3078+
$testComponent
3079+
->set('count', 99)
3080+
;
3081+
3082+
$this->assertStringContainsString('Count: 99', $testComponent->render());
3083+
3084+
// refresh the component
3085+
$testComponent->refresh();
3086+
3087+
// access the component object (in it's current state)
3088+
$component = $testComponent->component(); // MyComponent
3089+
3090+
$this->assertSame(99, $component->count);
3091+
3092+
// test a live action that redirects
3093+
$response = $testComponent->call('redirect')->response(); // Symfony\Component\HttpFoundation\Response
3094+
3095+
$this->assertSame(302, $response->getStatusCode());
3096+
}
3097+
}
3098+
3099+
.. note::
3100+
3101+
The ``InteractsWithLiveComponents`` trait can only be used in tests that extend
3102+
``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``.
3103+
30363104
Backward Compatibility promise
30373105
------------------------------
30383106

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\LiveComponent\Test;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\TwigComponent\ComponentFactory;
16+
17+
/**
18+
* @author Kevin Bond <kevinbond@gmail.com>
19+
*/
20+
trait InteractsWithLiveComponents
21+
{
22+
protected function createLiveComponent(string $name, array $data = []): TestLiveComponent
23+
{
24+
if (!$this instanceof KernelTestCase) {
25+
throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
26+
}
27+
28+
/** @var ComponentFactory $factory */
29+
$factory = self::getContainer()->get('ux.twig_component.component_factory');
30+
$metadata = $factory->metadataFor($name);
31+
32+
if (!$metadata->get('live')) {
33+
throw new \LogicException(sprintf('The "%s" component is not a live component.', $name));
34+
}
35+
36+
return new TestLiveComponent(
37+
$metadata,
38+
$data,
39+
$factory,
40+
self::getContainer()->get('test.client'),
41+
self::getContainer()->get('ux.live_component.component_hydrator'),
42+
self::getContainer()->get('ux.live_component.metadata_factory'),
43+
self::getContainer()->get('router'),
44+
);
45+
}
46+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\LiveComponent\Test;
13+
14+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
17+
use Symfony\UX\LiveComponent\LiveComponentHydrator;
18+
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
19+
use Symfony\UX\TwigComponent\ComponentFactory;
20+
use Symfony\UX\TwigComponent\ComponentMetadata;
21+
use Symfony\UX\TwigComponent\MountedComponent;
22+
use Symfony\UX\TwigComponent\Test\RenderedComponent;
23+
24+
/**
25+
* @author Kevin Bond <kevinbond@gmail.com>
26+
*/
27+
final class TestLiveComponent
28+
{
29+
/**
30+
* @internal
31+
*/
32+
public function __construct(
33+
private ComponentMetadata $metadata,
34+
array $data,
35+
private ComponentFactory $factory,
36+
private KernelBrowser $client,
37+
private LiveComponentHydrator $hydrator,
38+
private LiveComponentMetadataFactory $metadataFactory,
39+
private UrlGeneratorInterface $router,
40+
) {
41+
$this->client->catchExceptions(false);
42+
43+
$mounted = $this->factory->create($this->metadata->getName(), $data);
44+
$props = $this->hydrator->dehydrate(
45+
$mounted->getComponent(),
46+
$mounted->getAttributes(),
47+
$this->metadataFactory->getMetadata($mounted->getName())
48+
);
49+
50+
$this->client->request('GET', $this->router->generate(
51+
$this->metadata->get('route'),
52+
[
53+
'_live_component' => $this->metadata->getName(),
54+
'props' => json_encode($props->getProps(), flags: \JSON_THROW_ON_ERROR),
55+
]
56+
));
57+
}
58+
59+
public function render(): RenderedComponent
60+
{
61+
return new RenderedComponent($this->response()->getContent());
62+
}
63+
64+
public function component(): object
65+
{
66+
$component = $this->factory->get($this->metadata->getName());
67+
$componentAttributes = $this->hydrator->hydrate(
68+
$component,
69+
$this->props(),
70+
[],
71+
$this->metadataFactory->getMetadata($this->metadata->getName()),
72+
);
73+
74+
return (new MountedComponent($this->metadata->getName(), $component, $componentAttributes))->getComponent();
75+
}
76+
77+
/**
78+
* @param array<string,mixed> $arguments
79+
*/
80+
public function call(string $action, array $arguments = []): self
81+
{
82+
return $this->request(['args' => $arguments], $action);
83+
}
84+
85+
/**
86+
* @param array<string,mixed> $arguments
87+
*/
88+
public function emit(string $event, array $arguments = []): self
89+
{
90+
return $this->call($event, $arguments);
91+
}
92+
93+
public function set(string $prop, mixed $value): self
94+
{
95+
return $this->request(['updated' => [$prop => $value]]);
96+
}
97+
98+
public function refresh(): self
99+
{
100+
return $this->request();
101+
}
102+
103+
public function response(): Response
104+
{
105+
return $this->client->getResponse();
106+
}
107+
108+
private function request(array $content = [], string $action = null): self
109+
{
110+
$csrfToken = $this->csrfToken();
111+
112+
$this->client->request(
113+
'POST',
114+
$this->router->generate(
115+
$this->metadata->get('route'),
116+
array_filter([
117+
'_live_component' => $this->metadata->getName(),
118+
'_live_action' => $action,
119+
])
120+
),
121+
parameters: ['data' => json_encode(array_merge($content, ['props' => $this->props()]))],
122+
server: $csrfToken ? ['HTTP_X_CSRF_TOKEN' => $csrfToken] : [],
123+
);
124+
125+
return $this;
126+
}
127+
128+
private function props(): array
129+
{
130+
$crawler = $this->client->getCrawler();
131+
132+
if (!\count($node = $crawler->filter('[data-live-props-value]'))) {
133+
throw new \LogicException('A live component action has redirected and you can no longer access the component.');
134+
}
135+
136+
return json_decode($node->attr('data-live-props-value'), true, flags: \JSON_THROW_ON_ERROR);
137+
}
138+
139+
private function csrfToken(): ?string
140+
{
141+
$crawler = $this->client->getCrawler();
142+
143+
if (!\count($node = $crawler->filter('[data-live-csrf-value]'))) {
144+
return null;
145+
}
146+
147+
return $node->attr('data-live-csrf-value');
148+
}
149+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\LiveComponent\Tests\Fixtures\Component;
13+
14+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
15+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
16+
use Symfony\UX\LiveComponent\Attribute\PreReRender;
17+
use Symfony\UX\LiveComponent\DefaultActionTrait;
18+
19+
#[AsLiveComponent('track_renders')]
20+
final class TrackRenders
21+
{
22+
use DefaultActionTrait;
23+
24+
#[LiveProp]
25+
public int $reRenders = 0;
26+
27+
#[PreReRender]
28+
public function preReRender(): void
29+
{
30+
++$this->reRenders;
31+
}
32+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div{{ attributes }}>
2+
Re-Render Count: {{ reRenders }}
3+
</div>

0 commit comments

Comments
 (0)