diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md index 699db588d47..84ed16c82dc 100644 --- a/src/LiveComponent/CHANGELOG.md +++ b/src/LiveComponent/CHANGELOG.md @@ -1,5 +1,24 @@ # CHANGELOG +## 2.27.0 + +- Add events assertions in `InteractsWithLiveComponents`: +```php +$testComponent = $this->createLiveComponent(name: 'MyComponent'); + +$renderedComponent = $testComponent->render(); + +// Assert that the component did emit an event named 'event' +$this->assertComponentEmitEvent($render, 'event') + // optionally, you can assert that the event was emitted with specific data... + ->withData(['arg1' => 'foo', 'arg2' => 'bar']) + // ... or only with a subset of data + ->withDataSubset(['arg1' => 'foo']); + +// Assert that the component did not emit an event named 'another-event' +$this->assertComponentNotEmitEvent($render, 'another-event'); +``` + ## 2.26.0 - `LiveProp`: Pass the property name as second parameter of the `modifier` callable diff --git a/src/LiveComponent/doc/index.rst b/src/LiveComponent/doc/index.rst index 290b07bb5dc..b59142e1ddc 100644 --- a/src/LiveComponent/doc/index.rst +++ b/src/LiveComponent/doc/index.rst @@ -3778,9 +3778,20 @@ uses Symfony's test client to render and make requests to your components:: // emit live events $testComponent ->emit('increaseEvent') - ->emit('increaseEvent', ['amount' => 2]) // emit a live event with arguments + ->emit('increaseEvent', ['amount' => 2, 'unit' => 'kg']) // emit a live event with arguments ; + // Assert that the event was emitted + $this->assertComponentEmitEvent($testComponent->render(), 'increaseEvent') + // optionally, you can assert that the event was emitted with specific data... + ->withData(['amount' => 2, 'unit' => 'kg']) + // ... or only with a subset of data + ->withDataSubset(['amount' => 2]) + ; + + // Assert that an event was not emitted + $this->assertComponentNotEmitEvent($testComponent->render(), 'decreaseEvent'); + // 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..11b2f1147fc 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\AssertEmittedEvent; use Symfony\UX\TwigComponent\ComponentFactory; /** @@ -44,4 +45,18 @@ protected function createLiveComponent(string $name, array $data = [], ?KernelBr self::getContainer()->get('router'), ); } + + protected function assertComponentEmitEvent(TestLiveComponent $testLiveComponent, string $expectedEventName): AssertEmittedEvent + { + $event = $testLiveComponent->getEmittedEvent($testLiveComponent->render(), $expectedEventName); + + self::assertNotNull($event, \sprintf('The component "%s" did not emit event "%s".', $testLiveComponent->getName(), $expectedEventName)); + + return new AssertEmittedEvent($this, $event['event'], $event['data']); + } + + protected function assertComponentNotEmitEvent(TestLiveComponent $testLiveComponent, string $eventName): void + { + self::assertNull($testLiveComponent->getEmittedEvent($testLiveComponent->render(), $eventName), \sprintf('The component "%s" did 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<string, int|float|string|bool|null>, 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<array{data: array<string, int|float|string|bool|null>, 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/src/Test/Util/AssertEmittedEvent.php b/src/LiveComponent/src/Test/Util/AssertEmittedEvent.php new file mode 100644 index 00000000000..850c2fe5c01 --- /dev/null +++ b/src/LiveComponent/src/Test/Util/AssertEmittedEvent.php @@ -0,0 +1,55 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * 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 AssertEmittedEvent +{ + /** + * @param array<string, int|float|string|bool|null> $data + */ + public function __construct( + private readonly TestCase $testCase, + private readonly string $eventName, + private readonly array $data, + ) { + } + + /** + * @return self + */ + public function withDataSubset(array $expectedEventData): object + { + foreach ($expectedEventData as $key => $value) { + $this->testCase::assertArrayHasKey($key, $this->data, \sprintf('The expected event "%s" data "%s" does not exists', $this->eventName, $key)); + $this->testCase::assertSame( + $value, + $this->data[$key], + \sprintf( + 'The event "%s" data "%s" expect to be "%s", but "%s" given.', + $this->eventName, + $key, + $value, + $this->data[$key] + ) + ); + } + + return $this; + } + + public function withData(array $expectedEventData): void + { + $this->testCase::assertEquals($expectedEventData, $this->data, \sprintf('The event "%s" data 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 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..ff5461ae806 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,76 @@ public function testSetLocaleRenderLocalizedComponent(): void $testComponent->setRouteLocale('de'); $this->assertStringContainsString('Locale: de', $testComponent->render()); } + + public function testAssertComponentEmitEvent(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->assertComponentEmitEvent($testComponent, 'event1') + ->withData([ + 'foo' => 'bar', + 'bar' => 'foo', + ]); + } + + public function testAssertComponentEmitEventFails(): void + { + $testComponent = $this->createLiveComponent('component_with_emit'); + + $testComponent->call('actionThatEmits'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('The event "event1" data is different than expected.'); + $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 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 event "event1" data is different than expected.'); + $this->assertComponentEmitEvent($testComponent, 'event1')->withData([ + 'foo' => 'bar', + 'foo2' => 'bar2', + ]); + } }