diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst index 290b07bb5dc..14753abde14 100644 --- a/src/LiveComponent/doc/index.rst +++ b/src/LiveComponent/doc/index.rst @@ -3781,6 +3781,12 @@ uses Symfony's test client to render and make requests to your components:: ->emit('increaseEvent', ['amount' => 2]) // emit a live event with arguments ; + // Assert that the event was emitted + $this->componentHasEmittedEvent($testComponent->render(), 'increaseEvent') + ->withData(['amount' => 2]) + ->withDataSubset(['amount' => 2]) // test partial parameters + ; + // set live props $testComponent ->set('count', 99) diff --git a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php index 377fbad1f34..2462f81694c 100644 --- a/src/LiveComponent/src/Test/InteractsWithLiveComponents.php +++ b/src/LiveComponent/src/Test/InteractsWithLiveComponents.php @@ -44,4 +44,56 @@ protected function createLiveComponent(string $name, array $data = [], ?KernelBr self::getContainer()->get('router'), ); } + + /** + * @return object{withData: callable(array): void, withDataSubset: callable(array): object} + */ + protected function assertComponentEmitEvent(TestLiveComponent $testLiveComponent, string $expectedEventName): object + { + $event = $testLiveComponent->getEmittedEvent($testLiveComponent->render(), $expectedEventName); + + $this->assertNotNull($event, \sprintf('The component "%s" did not emit event "%s".', $testLiveComponent->getName(), $expectedEventName)); + + return new class($this, $event['event'], $event['data']) { + /** + * @param array $data + */ + public function __construct(private KernelTestCase $parent, private readonly string $eventName, private readonly array $data) + { + } + + /** + * @return self + */ + public function withDataSubset(array $expectedEventData): object + { + foreach ($expectedEventData as $key => $value) { + $this->parent->assertArrayHasKey($key, $this->data, \sprintf('The expected event "%s" data "%s" does not exists', $this->eventName, $key)); + $this->parent->assertSame( + $value, + $this->data[$key], + \sprintf( + 'The expected event "%s" data "%s" expected "%s" but "%s" given', + $this->eventName, + $key, + $value, + $this->data[$key] + ) + ); + } + + return $this; + } + + public function withData(array $expectedEventData): void + { + $this->parent->assertEquals($expectedEventData, $this->data, \sprintf('The expected event "%s" data does not match.', $this->eventName)); + } + }; + } + + protected function assertComponentNotEmitEvent(TestLiveComponent $testLiveComponent, string $eventName): void + { + $this->assertNull($testLiveComponent->getEmittedEvent($testLiveComponent->render(), $eventName), \sprintf('The component "%s" did not emit event "%s".', $testLiveComponent->getName(), $eventName)); + } } diff --git a/src/LiveComponent/src/Test/TestLiveComponent.php b/src/LiveComponent/src/Test/TestLiveComponent.php index 0e587a38256..580c8f8415f 100644 --- a/src/LiveComponent/src/Test/TestLiveComponent.php +++ b/src/LiveComponent/src/Test/TestLiveComponent.php @@ -229,4 +229,39 @@ private function flattenFormValues(array $values, string $prefix = ''): array return $result; } + + /** + * @return ?array{data: array, event: non-empty-string} + */ + public function getEmittedEvent(RenderedComponent $render, string $eventName): ?array + { + $events = $this->getEmittedEvents($render); + + foreach ($events as $event) { + if ($event['event'] === $eventName) { + return $event; + } + } + + return null; + } + + /** + * @return array, event: non-empty-string}> + */ + public function getEmittedEvents(RenderedComponent $render): array + { + $emit = $render->crawler()->filter('[data-live-name-value]')->attr('data-live-events-to-emit-value'); + + if (null === $emit) { + return []; + } + + return json_decode($emit, associative: true, flags: \JSON_THROW_ON_ERROR); + } + + public function getName(): string + { + return $this->metadata->getName(); + } } diff --git a/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php b/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php index 2b8325c81c4..14753bfbf66 100644 --- a/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php +++ b/src/LiveComponent/tests/Fixtures/Component/ComponentWithEmit.php @@ -29,7 +29,7 @@ final class ComponentWithEmit #[LiveAction] public function actionThatEmits(): void { - $this->emit('event1', ['foo' => 'bar']); + $this->emit('event1', ['foo' => 'bar', 'bar' => 'foo']); $this->events = $this->liveResponder->getEventsToEmit(); } diff --git a/src/LiveComponent/tests/Functional/LiveResponderTest.php b/src/LiveComponent/tests/Functional/LiveResponderTest.php index 8d533164eb9..992a42ecef3 100644 --- a/src/LiveComponent/tests/Functional/LiveResponderTest.php +++ b/src/LiveComponent/tests/Functional/LiveResponderTest.php @@ -35,7 +35,7 @@ public function testComponentCanEmitEvents(): void ]) ->assertSuccessful() ->assertSee('Event: event1') - ->assertSee('Data: {"foo":"bar"}'); + ->assertSee('Data: {"foo":"bar","bar":"foo"}'); } public function testComponentCanDispatchBrowserEvents(): void diff --git a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php index 097e4d770f7..91a6b6f0909 100644 --- a/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php +++ b/src/LiveComponent/tests/Functional/Test/InteractsWithLiveComponentsTest.php @@ -11,6 +11,7 @@ namespace Symfony\UX\LiveComponent\Tests\Functional\Test; +use PHPUnit\Framework\AssertionFailedError; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\User\InMemoryUser; @@ -217,4 +218,75 @@ public function testSetLocaleRenderLocalizedComponent(): void $testComponent->setRouteLocale('de'); $this->assertStringContainsString('Locale: de', $testComponent->render()); } + + public function testComponentEmitsExpectedEventData(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->assertComponentEmitEvent($testComponent, 'event1')->withData([ + 'foo' => 'bar', + 'bar' => 'foo', + ]); + } + + public function testComponentEmitsExpectedEventDataFails(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The expected event "event1" data does not match'); + $this->assertComponentEmitEvent($testComponent, 'event1')->withData([ + 'foo' => 'bar', + ]); + } + + public function testComponentEmitsExpectedPartialEventData(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->assertComponentEmitEvent($testComponent, 'event1') + ->withDataSubset(['foo' => 'bar']) + ->withDataSubset(['bar' => 'foo']) + ; + } + + public function testComponentDoesNotEmitUnexpectedEvent(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->assertComponentNotEmitEvent($testComponent, 'event2'); + } + + public function testComponentDoesNotEmitUnexpectedEventFails(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The component "component_with_emit" did not emit event "event1".'); + $this->assertComponentNotEmitEvent($testComponent, 'event1'); + } + + public function testComponentEmitsEventWithIncorrectDataFails(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The expected event "event1" data does not match.'); + $this->assertComponentEmitEvent($testComponent, 'event1')->withData([ + 'foo' => 'bar', + 'foo2' => 'bar2', + ]); + } }