diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst index 78f4b5a0cfc..2204ffaa59a 100644 --- a/src/LiveComponent/doc/index.rst +++ b/src/LiveComponent/doc/index.rst @@ -3851,6 +3851,23 @@ uses Symfony's test client to render and make requests to your components:: // Assert that an event was not emitted $this->assertComponentNotEmitEvent($testComponent->render(), 'decreaseEvent'); + // dispatch events + $testComponent + ->dispatchBrowserEvent('browserEvent') + ->dispatchBrowserEvent('browserEvent', ['amount' => 2, 'unit' => 'kg']) // dispatch a browser event with arguments + ; + + // Assert that the event was dispatched + $this->assertComponentDispatchEvent($testComponent->render(), 'browserEvent') + // optionally, you can assert that the event was dispatched with specific data... + ->withPayload(['amount' => 2, 'unit' => 'kg']) + // ... or only with a subset of data + ->withPayloadSubset(['amount' => 2]) + ; + + // Assert that an event was not dispatched + $this->assertComponentNotDispatchEvent($testComponent->render(), 'otherBrowserEvent'); + // set live props $testComponent ->set('count', 99) diff --git a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php index 11b2f1147fc..05ceda69a29 100644 --- a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php +++ b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\LiveComponent\Test\Util\AssertDispatchedEvent; use Symfony\UX\LiveComponent\Test\Util\AssertEmittedEvent; use Symfony\UX\TwigComponent\ComponentFactory; @@ -59,4 +60,24 @@ protected function assertComponentNotEmitEvent(TestLiveComponent $testLiveCompon { self::assertNull($testLiveComponent->getEmittedEvent($testLiveComponent->render(), $eventName), \sprintf('The component "%s" did emit event "%s".', $testLiveComponent->getName(), $eventName)); } + + protected function assertComponentDispatchEvent(TestLiveComponent $testLiveComponent, string $expectedEventName): AssertDispatchedEvent + { + $event = $testLiveComponent->getDispatchedEvent($testLiveComponent->render(), $expectedEventName); + + self::assertNotNull( + $event, + \sprintf('The component "%s" did no dispatch event "%s".', $testLiveComponent->getName(), $expectedEventName) + ); + + return new AssertDispatchedEvent($this, $event['event'], $event['payload']); + } + + protected function assertComponentNotDispatchEvent(TestLiveComponent $testLiveComponent, string $eventName): void + { + self::assertNull( + $testLiveComponent->getDispatchedEvent($testLiveComponent->render(), $eventName), + \sprintf('The component "%s" did dispatch event "%s".', $testLiveComponent->getName(), $eventName) + ); + } } diff --git a/src/LiveComponent/src/Test/TestLiveComponent.php b/src/LiveComponent/src/Test/TestLiveComponent.php index 580c8f8415f..66349e5f2d8 100644 --- a/src/LiveComponent/src/Test/TestLiveComponent.php +++ b/src/LiveComponent/src/Test/TestLiveComponent.php @@ -12,6 +12,7 @@ namespace Symfony\UX\LiveComponent\Test; use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -251,7 +252,7 @@ public function getEmittedEvent(RenderedComponent $render, string $eventName): ? */ public function getEmittedEvents(RenderedComponent $render): array { - $emit = $render->crawler()->filter('[data-live-name-value]')->attr('data-live-events-to-emit-value'); + $emit = $this->getComponentNameValue($render)->attr('data-live-events-to-emit-value'); if (null === $emit) { return []; @@ -260,6 +261,41 @@ public function getEmittedEvents(RenderedComponent $render): array return json_decode($emit, associative: true, flags: \JSON_THROW_ON_ERROR); } + /** + * @return ?array{data: array, event: non-empty-string} + */ + public function getDispatchedEvent(RenderedComponent $render, string $eventName): ?array + { + $events = $this->getDispatchedEvents($render); + + foreach ($events as $event) { + if ($event['event'] === $eventName) { + return $event; + } + } + + return null; + } + + /** + * @return array, event: non-empty-string}> + */ + public function getDispatchedEvents(RenderedComponent $render): array + { + $dispatch = $this->getComponentNameValue($render)->attr('data-live-events-to-dispatch-value'); + + if (null === $dispatch) { + return []; + } + + return json_decode($dispatch, associative: true, flags: \JSON_THROW_ON_ERROR); + } + + private function getComponentNameValue(RenderedComponent $render): Crawler + { + return $render->crawler()->filter('[data-live-name-value]'); + } + public function getName(): string { return $this->metadata->getName(); diff --git a/src/LiveComponent/src/Test/Util/AssertDispatchedEvent.php b/src/LiveComponent/src/Test/Util/AssertDispatchedEvent.php new file mode 100644 index 00000000000..ccee7c2d44c --- /dev/null +++ b/src/LiveComponent/src/Test/Util/AssertDispatchedEvent.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Test\Util; + +use PHPUnit\Framework\TestCase; + +final class AssertDispatchedEvent +{ + /** + * @param array $payload + */ + public function __construct( + private readonly TestCase $testCase, + private readonly string $eventName, + private readonly array $payload, + ) { + } + + /** + * @return self + */ + public function withPayloadSubset(array $expectedEventPayload): object + { + foreach ($expectedEventPayload as $key => $value) { + $this->testCase::assertArrayHasKey($key, $this->payload, \sprintf('The expected event "%s" data "%s" does not exists', $this->eventName, $key)); + $this->testCase::assertSame( + $value, + $this->payload[$key], + \sprintf( + 'The event "%s" data "%s" expect to be "%s", but "%s" given.', + $this->eventName, + $key, + $value, + $this->payload[$key] + ) + ); + } + + return $this; + } + + public function withPayload(array $expectedEventPayload): void + { + $this->testCase::assertEquals( + $expectedEventPayload, + $this->payload, + \sprintf('The event "%s" payload is different than expected.', $this->eventName) + ); + } +} diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php index 14753bfbf66..e4a60075c95 100644 --- a/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php @@ -24,7 +24,8 @@ final class ComponentWithEmit use DefaultActionTrait; use ComponentToolsTrait; - public $events = []; + public array $events = []; + public array $dispatchEvents = []; #[LiveAction] public function actionThatEmits(): void @@ -36,9 +37,13 @@ public function actionThatEmits(): void #[LiveAction] public function actionThatDispatchesABrowserEvent(): void { - $this->liveResponder->dispatchBrowserEvent( + $this->dispatchBrowserEvent( 'browser-event', - ['fooKey' => 'barVal'], + [ + 'fooKey' => 'barVal', + 'barKey' => 'fooVal', + ], ); + $this->dispatchEvents = $this->liveResponder->getBrowserEventsToDispatch(); } } diff --git a/src/LiveComponent/tests/Fixtures/templates/components/component_with_emit.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/component_with_emit.html.twig index f02e61af8ca..7b494305d59 100644 --- a/src/LiveComponent/tests/Fixtures/templates/components/component_with_emit.html.twig +++ b/src/LiveComponent/tests/Fixtures/templates/components/component_with_emit.html.twig @@ -5,4 +5,9 @@ Event: {{ event.event }}
Data: {{ event.data|json_encode|raw }}
{% endfor %} + + {% for event in dispatchEvents %} + Event: {{ event.event }}
+ Payload: {{ event.payload|json_encode|raw }}
+ {% endfor %} diff --git a/src/LiveComponent/tests/Functional/LiveResponderTest.php b/src/LiveComponent/tests/Functional/LiveResponderTest.php index 0ba92e5a9be..12c32d8b294 100644 --- a/src/LiveComponent/tests/Functional/LiveResponderTest.php +++ b/src/LiveComponent/tests/Functional/LiveResponderTest.php @@ -49,6 +49,8 @@ public function testComponentCanDispatchBrowserEvents() 'body' => ['data' => json_encode(['props' => $dehydrated->getProps()])], ]) ->assertSuccessful() + ->assertSee('Event: browser-event') + ->assertSee('Payload: {"fooKey":"barVal","barKey":"fooVal"}') ->crawler() ; @@ -57,7 +59,7 @@ public function testComponentCanDispatchBrowserEvents() $this->assertNotNull($browserDispatch); $browserDispatchData = json_decode($browserDispatch, true); $this->assertSame([ - ['event' => 'browser-event', 'payload' => ['fooKey' => 'barVal']], + ['event' => 'browser-event', 'payload' => ['fooKey' => 'barVal', 'barKey' => 'fooVal']], ], $browserDispatchData); } } diff --git a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php index e1654f2adbd..16c118e7d1a 100644 --- a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php +++ b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php @@ -290,4 +290,76 @@ public function testComponentEmitsEventWithIncorrectDataFails() 'foo2' => 'bar2', ]); } + + public function testAssertComponentDispatchEvent() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->assertComponentDispatchEvent($testComponent, 'browser-event') + ->withPayload([ + 'fooKey' => 'barVal', + 'barKey' => 'fooVal', + ]); + } + + public function testAssertComponentDispatchEventFails() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The event "browser-event" payload is different than expected.'); + $this->assertComponentDispatchEvent($testComponent, 'browser-event')->withPayload([ + 'fooKey' => 'barVal', + ]); + } + + public function testComponentDispatchesExpectedPartialEventData() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->assertComponentDispatchEvent($testComponent, 'browser-event') + ->withPayloadSubset(['fooKey' => 'barVal']) + ->withPayloadSubset(['barKey' => 'fooVal']) + ; + } + + public function testComponentDoesNotDispatchUnexpectedEvent() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->assertComponentNotDispatchEvent($testComponent, 'browser-event2'); + } + + public function testComponentDoesNotDispatchUnexpectedEventFails() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The component "component_with_emit" did dispatch event "browser-event".'); + $this->assertComponentNotDispatchEvent($testComponent, 'browser-event'); + } + + public function testComponentDispatchesEventWithIncorrectDataFails() + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatDispatchesABrowserEvent'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The event "browser-event" payload is different than expected.'); + $this->assertComponentDispatchEvent($testComponent, 'browser-event')->withPayload([ + 'fooKey' => 'barVal', + 'fooKey2' => 'barVal2', + ]); + } }