Skip to content

Commit 4b06ebd

Browse files
committed
feat(live): add test helpers
1 parent b2c00e6 commit 4b06ebd

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-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
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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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\Routing\Generator\UrlGeneratorInterface;
16+
use Symfony\UX\LiveComponent\LiveComponentHydrator;
17+
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory;
18+
use Symfony\UX\TwigComponent\ComponentFactory;
19+
use Symfony\UX\TwigComponent\ComponentMetadata;
20+
use Symfony\UX\TwigComponent\MountedComponent;
21+
use Symfony\UX\TwigComponent\Test\RenderedComponent;
22+
23+
/**
24+
* @author Kevin Bond <kevinbond@gmail.com>
25+
*/
26+
final class TestLiveComponent
27+
{
28+
private string $rendered;
29+
private array $props;
30+
private string $csrfToken;
31+
private object $component;
32+
33+
/**
34+
* @internal
35+
*/
36+
public function __construct(
37+
private ComponentMetadata $metadata,
38+
array $data,
39+
private ComponentFactory $factory,
40+
private KernelBrowser $client,
41+
private LiveComponentHydrator $hydrator,
42+
private LiveComponentMetadataFactory $metadataFactory,
43+
private UrlGeneratorInterface $router,
44+
) {
45+
$this->client->catchExceptions(false);
46+
47+
$mounted = $this->factory->create($this->metadata->getName(), $data);
48+
$props = $this->hydrator->dehydrate(
49+
$mounted->getComponent(),
50+
$mounted->getAttributes(),
51+
$this->metadataFactory->getMetadata($mounted->getName())
52+
);
53+
54+
$this->client->request('GET', $this->router->generate(
55+
$this->metadata->get('route'),
56+
[
57+
'_live_component' => $this->metadata->getName(),
58+
'props' => json_encode($props->getProps()),
59+
]
60+
));
61+
62+
$this->updateState();
63+
}
64+
65+
public function render(): RenderedComponent
66+
{
67+
return new RenderedComponent($this->rendered);
68+
}
69+
70+
public function component(): object
71+
{
72+
if (isset($this->component)) {
73+
return $this->component;
74+
}
75+
76+
$component = $this->factory->get($this->metadata->getName());
77+
$componentAttributes = $this->hydrator->hydrate(
78+
$component,
79+
$this->props,
80+
[],
81+
$this->metadataFactory->getMetadata($this->metadata->getName()),
82+
);
83+
84+
return $this->component = (new MountedComponent($this->metadata->getName(), $component, $componentAttributes))->getComponent();
85+
}
86+
87+
/**
88+
* @param array<string,mixed> $arguments
89+
*/
90+
public function call(string $action, array $arguments = []): self
91+
{
92+
return $this->request(['args' => $arguments], $action);
93+
}
94+
95+
/**
96+
* @param array<string,mixed> $arguments
97+
*/
98+
public function emit(string $event, array $arguments = []): self
99+
{
100+
return $this->call($event, $arguments);
101+
}
102+
103+
public function set(string $prop, mixed $value): self
104+
{
105+
return $this->request(['updated' => [$prop => $value]]);
106+
}
107+
108+
private function request(array $content = [], ?string $action = null): self
109+
{
110+
$this->client->request(
111+
'POST',
112+
$this->router->generate(
113+
$this->metadata->get('route'),
114+
array_filter([
115+
'_live_component' => $this->metadata->getName(),
116+
'_live_action' => $action,
117+
])
118+
),
119+
parameters: ['data' => json_encode(array_merge($content, ['props' => $this->props]))],
120+
server: ['HTTP_X_CSRF_TOKEN' => $this->csrfToken],
121+
);
122+
123+
return $this->updateState();
124+
}
125+
126+
private function updateState(): self
127+
{
128+
$crawler = $this->client->getCrawler();
129+
130+
$this->props = json_decode($crawler->filter('[data-live-props-value]')->attr('data-live-props-value'), true, flags: \JSON_THROW_ON_ERROR);
131+
$this->csrfToken = $crawler->filter('[data-live-csrf-value]')->attr('data-live-csrf-value');
132+
$this->rendered = $this->client->getResponse()->getContent();
133+
134+
unset($this->component);
135+
136+
return $this;
137+
}
138+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent\Tests\Functional\Test;
4+
5+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
6+
use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
7+
use Symfony\UX\LiveComponent\Tests\Fixtures\Component\Component2;
8+
9+
/**
10+
* @author Kevin Bond <kevinbond@gmail.com>
11+
*/
12+
final class InteractsWithLiveComponentsTest extends KernelTestCase
13+
{
14+
use InteractsWithLiveComponents;
15+
16+
public function testCanRenderInitialData(): void
17+
{
18+
$testComponent = $this->createLiveComponent('component2');
19+
20+
$this->assertStringContainsString('Count: 1', (string) $testComponent->render());
21+
$this->assertSame(1, $testComponent->component()->count);
22+
}
23+
24+
public function testCanCreateWithClassString(): void
25+
{
26+
$testComponent = $this->createLiveComponent(Component2::class);
27+
28+
$this->assertStringContainsString('Count: 1', (string) $testComponent->render());
29+
$this->assertSame(1, $testComponent->component()->count);
30+
}
31+
32+
public function testCanCallLiveAction(): void
33+
{
34+
$testComponent = $this->createLiveComponent('component2');
35+
36+
$this->assertStringContainsString('Count: 1', $testComponent->render());
37+
$this->assertSame(1, $testComponent->component()->count);
38+
39+
$testComponent->call('increase');
40+
41+
$this->assertStringContainsString('Count: 2', $testComponent->render());
42+
$this->assertSame(2, $testComponent->component()->count);
43+
}
44+
45+
public function testCanCallLiveActionWithArguments(): void
46+
{
47+
$testComponent = $this->createLiveComponent('component6');
48+
49+
$this->assertStringContainsString('Arg1: not provided', $testComponent->render());
50+
$this->assertStringContainsString('Arg2: not provided', $testComponent->render());
51+
$this->assertStringContainsString('Arg3: not provided', $testComponent->render());
52+
$this->assertNull($testComponent->component()->arg1);
53+
$this->assertNull($testComponent->component()->arg2);
54+
$this->assertNull($testComponent->component()->arg3);
55+
56+
$testComponent->call('inject', ['arg1' => 'hello', 'arg2' => 666, 'custom' => '33.3']);
57+
58+
$this->assertStringContainsString('Arg1: hello', $testComponent->render());
59+
$this->assertStringContainsString('Arg2: 666', $testComponent->render());
60+
$this->assertStringContainsString('Arg3: 33.3', $testComponent->render());
61+
$this->assertSame('hello', $testComponent->component()->arg1);
62+
$this->assertSame(666, $testComponent->component()->arg2);
63+
$this->assertSame(33.3, $testComponent->component()->arg3);
64+
}
65+
66+
public function testCanSetLiveProp(): void
67+
{
68+
$testComponent = $this->createLiveComponent('component_with_writable_props');
69+
70+
$this->assertStringContainsString('Count: 1', $testComponent->render());
71+
$this->assertSame(1, $testComponent->component()->count);
72+
73+
$testComponent->set('count', 100);
74+
75+
$this->assertStringContainsString('Count: 100', $testComponent->render());
76+
$this->assertSame(100, $testComponent->component()->count);
77+
}
78+
}

0 commit comments

Comments
 (0)