Options array supported by CommandTester.
+ */
+ private function configureOptions(array $parameters): array
+ {
+ $options = [];
+
+ if (in_array('--ansi', $parameters, true)) {
+ $options['decorated'] = true;
+ } elseif (in_array('--no-ansi', $parameters, true)) {
+ $options['decorated'] = false;
+ }
+
+ if (in_array('--no-interaction', $parameters, true) || in_array('-n', $parameters, true)) {
+ $options['interactive'] = false;
+ }
+
+ if (in_array('--quiet', $parameters, true) || in_array('-q', $parameters, true)) {
+ $options['verbosity'] = OutputInterface::VERBOSITY_QUIET;
+ $options['interactive'] = false;
+ }
+
+ if (in_array('-vvv', $parameters, true)
+ || in_array('--verbose=3', $parameters, true)
+ || (isset($parameters['--verbose']) && $parameters['--verbose'] === 3)
+ ) {
+ $options['verbosity'] = OutputInterface::VERBOSITY_DEBUG;
+ } elseif (in_array('-vv', $parameters, true)
+ || in_array('--verbose=2', $parameters, true)
+ || (isset($parameters['--verbose']) && $parameters['--verbose'] === 2)
+ ) {
+ $options['verbosity'] = OutputInterface::VERBOSITY_VERY_VERBOSE;
+ } elseif (in_array('-v', $parameters, true)
+ || in_array('--verbose=1', $parameters, true)
+ || in_array('--verbose', $parameters, true)
+ || (isset($parameters['--verbose']) && $parameters['--verbose'] === 1)
+ ) {
+ $options['verbosity'] = OutputInterface::VERBOSITY_VERBOSE;
+ }
+
+ return $options;
+ }
+
protected function grabKernelService(): KernelInterface
{
return $this->grabService('kernel');
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php
index 2ea3606f..6f9f0bc4 100644
--- a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php
@@ -6,7 +6,6 @@
use Doctrine\ORM\EntityRepository;
use function class_exists;
-use function get_class;
use function interface_exists;
use function is_object;
use function is_string;
@@ -27,7 +26,6 @@ trait DoctrineAssertionsTrait
*
* @param string $entityClass The entity class
* @param array $criteria Optional query criteria
- * @return int
*/
public function grabNumRecords(string $entityClass, array $criteria = []): int
{
@@ -55,11 +53,8 @@ public function grabNumRecords(string $entityClass, array $criteria = []): int
* $I->grabRepository(UserRepository::class);
* $I->grabRepository(UserRepositoryInterface::class);
* ```
- *
- * @param object|string $mixed
- * @return \Doctrine\ORM\EntityRepository|null
*/
- public function grabRepository($mixed): ?EntityRepository
+ public function grabRepository(object|string $mixed): ?EntityRepository
{
$entityRepoClass = EntityRepository::class;
$isNotARepo = function () use ($mixed): void {
@@ -70,6 +65,7 @@ public function grabRepository($mixed): ?EntityRepository
$getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo): ?EntityRepository {
if (!$repo = $this->grabService($mixed)) return null;
+ /** @var EntityRepository $repo */
if (!$repo instanceof $entityRepoClass) {
$isNotARepo();
return null;
@@ -79,7 +75,7 @@ public function grabRepository($mixed): ?EntityRepository
};
if (is_object($mixed)) {
- $mixed = get_class($mixed);
+ $mixed = $mixed::class;
}
if (interface_exists($mixed)) {
diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php
new file mode 100644
index 00000000..f25f9bf3
--- /dev/null
+++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php
@@ -0,0 +1,179 @@
+assertCheckboxChecked('agree_terms');
+ * ```
+ */
+ public function assertCheckboxChecked(string $fieldName, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked"), $message);
+ }
+
+ /**
+ * Asserts that the checkbox with the given name is not checked.
+ *
+ * ```php
+ * assertCheckboxNotChecked('subscribe');
+ * ```
+ */
+ public function assertCheckboxNotChecked(string $fieldName, string $message = ''): void
+ {
+ $this->assertThatCrawler(
+ new LogicalNot(
+ new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked")
+ ), $message
+ );
+ }
+
+ /**
+ * Asserts that the value of the form input with the given name does not equal the expected value.
+ *
+ * ```php
+ * assertInputValueNotSame('username', 'admin');
+ * ```
+ */
+ public function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message);
+ $this->assertThatCrawler(
+ new LogicalNot(
+ new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)
+ ), $message
+ );
+ }
+
+ /**
+ * Asserts that the value of the form input with the given name equals the expected value.
+ *
+ * ```php
+ * assertInputValueSame('username', 'johndoe');
+ * ```
+ */
+ public function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message);
+ $this->assertThatCrawler(
+ new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue),
+ $message
+ );
+ }
+
+ /**
+ * Asserts that the `` element contains the given title.
+ *
+ * ```php
+ * assertPageTitleContains('Welcome');
+ * ```
+ */
+ public function assertPageTitleContains(string $expectedTitle, string $message = ''): void
+ {
+ $this->assertSelectorTextContains('title', $expectedTitle, $message);
+ }
+
+ /**
+ * Asserts that the `` element equals the given title.
+ *
+ * ```php
+ * assertPageTitleSame('Home Page');
+ * ```
+ */
+ public function assertPageTitleSame(string $expectedTitle, string $message = ''): void
+ {
+ $this->assertSelectorTextSame('title', $expectedTitle, $message);
+ }
+
+ /**
+ * Asserts that the given selector matches at least one element in the response.
+ *
+ * ```php
+ * assertSelectorExists('.main-content');
+ * ```
+ */
+ public function assertSelectorExists(string $selector, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message);
+ }
+
+ /**
+ * Asserts that the given selector does not match at least one element in the response.
+ *
+ * ```php
+ * assertSelectorNotExists('.error');
+ * ```
+ */
+ public function assertSelectorNotExists(string $selector, string $message = ''): void
+ {
+ $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorExists($selector)), $message);
+ }
+
+ /**
+ * Asserts that the first element matching the given selector contains the expected text.
+ *
+ * ```php
+ * assertSelectorTextContains('h1', 'Dashboard');
+ * ```
+ */
+ public function assertSelectorTextContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message);
+ $this->assertThatCrawler(new CrawlerSelectorTextContains($selector, $text), $message);
+ }
+
+ /**
+ * Asserts that the first element matching the given selector does not contain the expected text.
+ *
+ * ```php
+ * assertSelectorTextNotContains('p', 'error');
+ * ```
+ */
+ public function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message);
+ $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorTextContains($selector, $text)), $message);
+ }
+
+ /**
+ * Asserts that the text of the first element matching the given selector equals the expected text.
+ *
+ * ```php
+ * assertSelectorTextSame('h1', 'Dashboard');
+ * ```
+ */
+ public function assertSelectorTextSame(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message);
+ $this->assertThatCrawler(new CrawlerSelectorTextSame($selector, $text), $message);
+ }
+
+ protected function assertThatCrawler(Constraint $constraint, string $message): void
+ {
+ $this->assertThat($this->getClient()->getCrawler(), $constraint, $message);
+ }
+}
diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php
index 64eca663..f761499b 100644
--- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php
@@ -5,44 +5,48 @@
namespace Codeception\Module\Symfony;
use Symfony\Component\HttpKernel\DataCollector\EventDataCollector;
-use Symfony\Component\VarDumper\Cloner\Data;
-use function get_class;
use function is_array;
use function is_object;
-use function strpos;
trait EventsAssertionsTrait
{
/**
- * Verifies that there were no orphan events during the test.
+ * Verifies that there were no events during the test.
+ * Both regular and orphan events are checked.
*
- * An orphan event is an event that was triggered by manually executing the
- * [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method
- * of the EventDispatcher but was not handled by any listener after it was dispatched.
+ * ```php
+ * dontSeeEvent();
+ * $I->dontSeeEvent('App\MyEvent');
+ * $I->dontSeeEvent(['App\MyEvent', 'App\MyOtherEvent']);
+ * ```
+ *
+ * @param string|string[]|null $expected
+ */
+ public function dontSeeEvent(array|string|null $expected = null): void
+ {
+ $actualEvents = [...array_column($this->getCalledListeners(), 'event')];
+ $actual = [$this->getOrphanedEvents(), $actualEvents];
+ $this->assertEventTriggered(false, $expected, $actual);
+ }
+
+ /**
+ * Verifies that one or more event listeners were not called during the test.
*
* ```php
* dontSeeOrphanEvent();
- * $I->dontSeeOrphanEvent('App\MyEvent');
- * $I->dontSeeOrphanEvent(new App\Events\MyEvent());
- * $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
+ * $I->dontSeeEventListenerIsCalled('App\MyEventListener');
+ * $I->dontSeeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']);
+ * $I->dontSeeEventListenerIsCalled('App\MyEventListener', 'my.event);
+ * $I->dontSeeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']);
* ```
*
- * @param string|object|string[] $expected
+ * @param class-string|class-string[] $expected
+ * @param string|string[] $events
*/
- public function dontSeeOrphanEvent($expected = null): void
+ public function dontSeeEventListenerIsCalled(array|object|string $expected, array|string $events = []): void
{
- $eventCollector = $this->grabEventCollector(__FUNCTION__);
-
- /** @var Data $data */
- $data = $eventCollector->getOrphanedEvents();
- $expected = is_array($expected) ? $expected : [$expected];
-
- if ($expected === null) {
- $this->assertSame(0, $data->count());
- } else {
- $this->assertEventNotTriggered($data, $expected);
- }
+ $this->assertListenerCalled(false, $expected, $events);
}
/**
@@ -55,21 +59,20 @@ public function dontSeeOrphanEvent($expected = null): void
* $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']);
* ```
*
- * @param string|object|string[] $expected
+ * @param object|string|string[] $expected
+ * @deprecated Use `dontSeeEventListenerIsCalled` instead.
*/
- public function dontSeeEventTriggered($expected): void
+ public function dontSeeEventTriggered(array|object|string $expected): void
{
- $eventCollector = $this->grabEventCollector(__FUNCTION__);
-
- /** @var Data $data */
- $data = $eventCollector->getCalledListeners();
- $expected = is_array($expected) ? $expected : [$expected];
-
- $this->assertEventNotTriggered($data, $expected);
+ trigger_error(
+ 'dontSeeEventTriggered is deprecated, please use dontSeeEventListenerIsCalled instead',
+ E_USER_DEPRECATED
+ );
+ $this->dontSeeEventListenerIsCalled($expected);
}
/**
- * Verifies that one or more orphan events were dispatched during the test.
+ * Verifies that there were no orphan events during the test.
*
* An orphan event is an event that was triggered by manually executing the
* [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method
@@ -77,22 +80,58 @@ public function dontSeeEventTriggered($expected): void
*
* ```php
* seeOrphanEvent('App\MyEvent');
- * $I->seeOrphanEvent(new App\Events\MyEvent());
- * $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
+ * $I->dontSeeOrphanEvent();
+ * $I->dontSeeOrphanEvent('App\MyEvent');
+ * $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
* ```
*
- * @param string|object|string[] $expected
+ * @param string|string[] $expected
*/
- public function seeOrphanEvent($expected): void
+ public function dontSeeOrphanEvent(array|string|null $expected = null): void
{
- $eventCollector = $this->grabEventCollector(__FUNCTION__);
+ $actual = [$this->getOrphanedEvents()];
+ $this->assertEventTriggered(false, $expected, $actual);
+ }
- /** @var Data $data */
- $data = $eventCollector->getOrphanedEvents();
- $expected = is_array($expected) ? $expected : [$expected];
+ /**
+ * Verifies that one or more events were dispatched during the test.
+ * Both regular and orphan events are checked.
+ *
+ * If you need to verify that expected event is not orphan,
+ * add `dontSeeOrphanEvent` call.
+ *
+ * ```php
+ * seeEvent('App\MyEvent');
+ * $I->seeEvent(['App\MyEvent', 'App\MyOtherEvent']);
+ * ```
+ *
+ * @param string|string[] $expected
+ */
+ public function seeEvent(array|string $expected): void
+ {
+ $actualEvents = [...array_column($this->getCalledListeners(), 'event')];
+ $actual = [$this->getOrphanedEvents(), $actualEvents];
+ $this->assertEventTriggered(true, $expected, $actual);
+ }
- $this->assertEventTriggered($data, $expected);
+ /**
+ * Verifies that one or more event listeners were called during the test.
+ *
+ * ```php
+ * seeEventListenerIsCalled('App\MyEventListener');
+ * $I->seeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']);
+ * $I->seeEventListenerIsCalled('App\MyEventListener', 'my.event);
+ * $I->seeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']);
+ * ```
+ *
+ * @param class-string|class-string[] $expected
+ * @param string|string[] $events
+ */
+ public function seeEventListenerIsCalled(array|object|string $expected, array|string $events = []): void
+ {
+ $this->assertListenerCalled(true, $expected, $events);
}
/**
@@ -105,69 +144,124 @@ public function seeOrphanEvent($expected): void
* $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']);
* ```
*
- * @param string|object|string[] $expected
+ * @param object|string|string[] $expected
+ * @deprecated Use `seeEventListenerIsCalled` instead.
*/
- public function seeEventTriggered($expected): void
+ public function seeEventTriggered(array|object|string $expected): void
{
- $eventCollector = $this->grabEventCollector(__FUNCTION__);
+ trigger_error(
+ 'seeEventTriggered is deprecated, please use seeEventListenerIsCalled instead',
+ E_USER_DEPRECATED
+ );
+ $this->seeEventListenerIsCalled($expected);
+ }
+
+ /**
+ * Verifies that one or more orphan events were dispatched during the test.
+ *
+ * An orphan event is an event that was triggered by manually executing the
+ * [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method
+ * of the EventDispatcher but was not handled by any listener after it was dispatched.
+ *
+ * ```php
+ * seeOrphanEvent('App\MyEvent');
+ * $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']);
+ * ```
+ *
+ * @param string|string[] $expected
+ */
+ public function seeOrphanEvent(array|string $expected): void
+ {
+ $actual = [$this->getOrphanedEvents()];
+ $this->assertEventTriggered(true, $expected, $actual);
+ }
- /** @var Data $data */
- $data = $eventCollector->getCalledListeners();
- $expected = is_array($expected) ? $expected : [$expected];
+ protected function getCalledListeners(): array
+ {
+ $eventCollector = $this->grabEventCollector(__FUNCTION__);
+ $calledListeners = $eventCollector->getCalledListeners($this->getDefaultDispatcher());
+ return [...$calledListeners->getValue(true)];
+ }
- $this->assertEventTriggered($data, $expected);
+ protected function getOrphanedEvents(): array
+ {
+ $eventCollector = $this->grabEventCollector(__FUNCTION__);
+ $orphanedEvents = $eventCollector->getOrphanedEvents($this->getDefaultDispatcher());
+ return [...$orphanedEvents->getValue(true)];
}
- protected function assertEventNotTriggered(Data $data, array $expected): void
+ protected function assertEventTriggered(bool $assertTrue, array|object|string|null $expected, array $actual): void
{
- $actual = $data->getValue(true);
-
- foreach ($expected as $expectedEvent) {
- $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent;
- $this->assertFalse(
- $this->eventWasTriggered($actual, (string)$expectedEvent),
- "The '{$expectedEvent}' event triggered"
- );
+ $actualEvents = array_merge(...$actual);
+
+ if ($assertTrue) $this->assertNotEmpty($actualEvents, 'No event was triggered');
+ if ($expected === null) {
+ $this->assertEmpty($actualEvents);
+ return;
+ }
+
+ $expected = is_object($expected) ? $expected::class : $expected;
+ foreach ((array)$expected as $expectedEvent) {
+ $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent;
+ $eventTriggered = in_array($expectedEvent, $actualEvents);
+
+ $message = $assertTrue
+ ? "The '{$expectedEvent}' event did not trigger"
+ : "The '{$expectedEvent}' event triggered";
+ $this->assertSame($assertTrue, $eventTriggered, $message);
}
}
- protected function assertEventTriggered(Data $data, array $expected): void
+ protected function assertListenerCalled(bool $assertTrue, array|object|string $expectedListeners, array|object|string $expectedEvents): void
{
- if ($data->count() === 0) {
- $this->fail('No event was triggered');
+ $expectedListeners = is_array($expectedListeners) ? $expectedListeners : [$expectedListeners];
+ $expectedEvents = is_array($expectedEvents) ? $expectedEvents : [$expectedEvents];
+
+ if (empty($expectedEvents)) {
+ $expectedEvents = [null];
+ } elseif (count($expectedListeners) > 1) {
+ $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.');
+ }
+
+ $actualEvents = $this->getCalledListeners();
+ if ($assertTrue && empty($actualEvents)) {
+ $this->fail('No event listener was called');
}
- $actual = $data->getValue(true);
+ foreach ($expectedListeners as $expectedListener) {
+ $expectedListener = is_object($expectedListener) ? $expectedListener::class : $expectedListener;
- foreach ($expected as $expectedEvent) {
- $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent;
- $this->assertTrue(
- $this->eventWasTriggered($actual, (string)$expectedEvent),
- "The '{$expectedEvent}' event did not trigger"
- );
+ foreach ($expectedEvents as $expectedEvent) {
+ $listenerCalled = $this->listenerWasCalled($expectedListener, $expectedEvent, $actualEvents);
+ $message = "The '{$expectedListener}' listener was called"
+ . ($expectedEvent ? " for the '{$expectedEvent}' event" : '');
+ $this->assertSame($assertTrue, $listenerCalled, $message);
+ }
}
}
- protected function eventWasTriggered(array $actual, string $expectedEvent): bool
+ private function listenerWasCalled(string $expectedListener, ?string $expectedEvent, array $actualEvents): bool
{
- $triggered = false;
-
- foreach ($actual as $actualEvent) {
- if (is_array($actualEvent)) { // Called Listeners
- if (strpos($actualEvent['pretty'], $expectedEvent) === 0) {
- $triggered = true;
- }
- } else { // Orphan Events
- if ($actualEvent === $expectedEvent) {
- $triggered = true;
- }
+ foreach ($actualEvents as $actualEvent) {
+ if (
+ isset($actualEvent['pretty'], $actualEvent['event'])
+ && str_starts_with($actualEvent['pretty'], $expectedListener)
+ && ($expectedEvent === null || $actualEvent['event'] === $expectedEvent)
+ ) {
+ return true;
}
}
- return $triggered;
+ return false;
+ }
+
+ protected function getDefaultDispatcher(): string
+ {
+ return 'event_dispatcher';
}
protected function grabEventCollector(string $function): EventDataCollector
{
return $this->grabCollector('events', $function);
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php
index 3b923059..f77403bd 100644
--- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php
@@ -12,6 +12,39 @@
trait FormAssertionsTrait
{
+ /**
+ * Asserts that value of the field of the first form matching the given selector does equal the expected value.
+ *
+ * ```php
+ * assertFormValue('#loginForm', 'username', 'john_doe');
+ * ```
+ */
+ public function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void
+ {
+ $node = $this->getCLient()->getCrawler()->filter($formSelector);
+ $this->assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector));
+ $values = $node->form()->getValues();
+ $this->assertArrayHasKey($fieldName, $values, $message ?: sprintf('Field "%s" not found in form "%s".', $fieldName, $formSelector));
+ $this->assertSame($value, $values[$fieldName]);
+ }
+
+ /**
+ * Asserts that the field of the first form matching the given selector does not have a value.
+ *
+ * ```php
+ * assertNoFormValue('#registrationForm', 'middle_name');
+ * ```
+ */
+ public function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void
+ {
+ $node = $this->getCLient()->getCrawler()->filter($formSelector);
+ $this->assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector));
+ $values = $node->form()->getValues();
+ $this->assertArrayNotHasKey($fieldName, $values, $message ?: sprintf('Field "%s" has a value in form "%s".', $fieldName, $formSelector));
+ }
+
/**
* Verifies that there are no errors bound to the submitted form.
*
@@ -42,16 +75,13 @@ public function dontSeeFormErrors(): void
* $I->seeFormErrorMessage('username');
* $I->seeFormErrorMessage('username', 'Username is empty');
* ```
- *
- * @param string $field
- * @param string|null $message
*/
public function seeFormErrorMessage(string $field, ?string $message = null): void
{
$formCollector = $this->grabFormCollector(__FUNCTION__);
if (!$forms = $formCollector->getData()->getValue(true)['forms']) {
- $this->fail('There are no forms on the current page.');
+ $this->fail('No forms found on the current page.');
}
$fields = [];
@@ -73,7 +103,7 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi
}
if (!in_array($field, $fields)) {
- $this->fail("the field '{$field}' does not exist in the form.");
+ $this->fail("The field '{$field}' does not exist in the form.");
}
if (!array_key_exists($field, $errors)) {
@@ -108,7 +138,6 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi
* If you want to specify the error messages, you can do so
* by sending an associative array instead, with the key being
* the name of the field and the error message the value.
- *
* This method will validate that the expected error message
* is contained in the actual error message, that is,
* you can specify either the entire error message or just a part of it:
@@ -116,7 +145,7 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi
* ```php
* seeFormErrorMessages([
- * 'address' => 'The address is too long'
+ * 'address' => 'The address is too long',
* 'telephone' => 'too short', // the full error message is 'The telephone is too short'
* ]);
* ```
@@ -171,4 +200,4 @@ protected function grabFormCollector(string $function): FormDataCollector
{
return $this->grabCollector('form', $function);
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php
new file mode 100644
index 00000000..f6f322eb
--- /dev/null
+++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php
@@ -0,0 +1,138 @@
+assertHttpClientRequest(
+ * 'https://example.com/api',
+ * 'POST',
+ * '{"data": "value"}',
+ * ['Authorization' => 'Bearer token']
+ * );
+ * ```
+ */
+ public function assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client'): void
+ {
+ $httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
+ $expectedRequestHasBeenFound = false;
+
+ if (!array_key_exists($httpClientId, $httpClientCollector->getClients())) {
+ $this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
+ }
+
+ foreach ($httpClientCollector->getClients()[$httpClientId]['traces'] as $trace) {
+ if (($expectedUrl !== $trace['info']['url'] && $expectedUrl !== $trace['url'])
+ || $expectedMethod !== $trace['method']
+ ) {
+ continue;
+ }
+
+ if (null !== $expectedBody) {
+ $actualBody = null;
+
+ if (null !== $trace['options']['body'] && null === $trace['options']['json']) {
+ $actualBody = is_string($trace['options']['body']) ? $trace['options']['body'] : $trace['options']['body']->getValue(true);
+ }
+
+ if (null === $trace['options']['body'] && null !== $trace['options']['json']) {
+ $actualBody = $trace['options']['json']->getValue(true);
+ }
+
+ if (!$actualBody) {
+ continue;
+ }
+
+ if ($expectedBody === $actualBody) {
+ $expectedRequestHasBeenFound = true;
+
+ if (!$expectedHeaders) {
+ break;
+ }
+ }
+ }
+
+ if ($expectedHeaders) {
+ $actualHeaders = $trace['options']['headers'] ?? [];
+
+ foreach ($actualHeaders as $headerKey => $actualHeader) {
+ if (array_key_exists($headerKey, $expectedHeaders)
+ && $expectedHeaders[$headerKey] === $actualHeader->getValue(true)
+ ) {
+ $expectedRequestHasBeenFound = true;
+ break 2;
+ }
+ }
+ }
+
+ $expectedRequestHasBeenFound = true;
+ break;
+ }
+
+ $this->assertTrue($expectedRequestHasBeenFound, 'The expected request has not been called: "' . $expectedMethod . '" - "' . $expectedUrl . '"');
+ }
+
+ /**
+ * Asserts that the given number of requests has been made on the HttpClient.
+ * By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID.
+ *
+ * ```php
+ * assertHttpClientRequestCount(3);
+ * ```
+ */
+ public function assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client'): void
+ {
+ $httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
+
+ $this->assertCount($count, $httpClientCollector->getClients()[$httpClientId]['traces']);
+ }
+
+ /**
+ * Asserts that the given URL has not been called using GET or the specified method.
+ * By default, it will check on the HttpClient, but a HttpClient id can be specified.
+ *
+ * ```php
+ * assertNotHttpClientRequest('https://example.com/unexpected', 'GET');
+ * ```
+ */
+ public function assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client'): void
+ {
+ $httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
+ $unexpectedUrlHasBeenFound = false;
+
+ if (!array_key_exists($httpClientId, $httpClientCollector->getClients())) {
+ $this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
+ }
+
+ foreach ($httpClientCollector->getClients()[$httpClientId]['traces'] as $trace) {
+ if (($unexpectedUrl === $trace['info']['url'] || $unexpectedUrl === $trace['url'])
+ && $expectedMethod === $trace['method']
+ ) {
+ $unexpectedUrlHasBeenFound = true;
+ break;
+ }
+ }
+
+ $this->assertFalse($unexpectedUrlHasBeenFound, sprintf('Unexpected URL called: "%s" - "%s"', $expectedMethod, $unexpectedUrl));
+ }
+
+ protected function grabHttpClientCollector(string $function): HttpClientDataCollector
+ {
+ return $this->grabCollector('http_client', $function);
+ }
+}
diff --git a/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php b/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php
new file mode 100644
index 00000000..4cd0266a
--- /dev/null
+++ b/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php
@@ -0,0 +1,60 @@
+amOnPage('/home');
+ * $I->dontSeeDeprecations();
+ * ```
+ *
+ * @param string $message Optional custom failure message.
+ */
+ public function dontSeeDeprecations(string $message = ''): void
+ {
+ $loggerCollector = $this->grabLoggerCollector(__FUNCTION__);
+ $logs = $loggerCollector->getProcessedLogs();
+
+ $foundDeprecations = [];
+
+ foreach ($logs as $log) {
+ if (isset($log['type']) && $log['type'] === 'deprecation') {
+ $msg = $log['message'];
+ if ($msg instanceof Data) {
+ $msg = $msg->getValue(true);
+ }
+ if (!is_string($msg)) {
+ $msg = (string)$msg;
+ }
+ $foundDeprecations[] = $msg;
+ }
+ }
+
+ $errorMessage = $message ?: sprintf(
+ "Found %d deprecation message%s in the log:\n%s",
+ count($foundDeprecations),
+ count($foundDeprecations) > 1 ? 's' : '',
+ implode("\n", array_map(static function ($msg) {
+ return " - " . $msg;
+ }, $foundDeprecations))
+ );
+
+ $this->assertEmpty($foundDeprecations, $errorMessage);
+ }
+
+ protected function grabLoggerCollector(string $function): LoggerDataCollector
+ {
+ return $this->grabCollector('logger', $function);
+ }
+}
diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php
index 9ead87ff..5a31e6d8 100644
--- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php
@@ -4,6 +4,8 @@
namespace Codeception\Module\Symfony;
+use PHPUnit\Framework\Constraint\LogicalNot;
+use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Event\MessageEvents;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
use Symfony\Component\Mailer\Test\Constraint as MailerConstraint;
@@ -12,39 +14,80 @@
trait MailerAssertionsTrait
{
/**
- * Checks that no email was sent.
- * The check is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
- * If your app performs a HTTP redirect, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; otherwise this check will *always* pass.
- * Starting with version 2.0.0, `codeception/module-symfony` requires your app to use [Symfony Mailer](https://symfony.com/doc/current/mailer.html). If your app still uses [Swift Mailer](https://symfony.com/doc/current/email.html), set your version constraint to `^1.6`.
+ * Asserts that the expected number of emails was sent.
+ *
+ * ```php
+ * assertEmailCount(2, 'smtp');
+ * ```
*/
- public function dontSeeEmailIsSent(): void
+ public function assertEmailCount(int $count, ?string $transport = null, string $message = ''): void
{
- $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount(0));
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport), $message);
}
/**
- * Checks if the given number of emails was sent (default `$expectedCount`: 1).
- * The check is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
- * If your app performs a HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first.
- * Starting with version 2.0.0, `codeception/module-symfony` requires your app to use [Symfony Mailer](https://symfony.com/doc/current/mailer.html). If your app still uses [Swift Mailer](https://symfony.com/doc/current/email.html), set your version constraint to `^1.6`.
+ * Asserts that the given mailer event is not queued.
+ * Use `getMailerEvent(int $index = 0, ?string $transport = null)` to retrieve a mailer event by index.
*
* ```php
* seeEmailIsSent(2);
+ * $event = $I->getMailerEvent();
+ * $I->assertEmailIsNotQueued($event);
* ```
+ */
+ public function assertEmailIsNotQueued(MessageEvent $event, string $message = ''): void
+ {
+ $this->assertThat($event, new LogicalNot(new MailerConstraint\EmailIsQueued()), $message);
+ }
+
+ /**
+ * Asserts that the given mailer event is queued.
+ * Use `getMailerEvent(int $index = 0, ?string $transport = null)` to retrieve a mailer event by index.
*
- * @param int $expectedCount The expected number of emails sent
+ * ```php
+ * getMailerEvent();
+ * $I->assertEmailIsQueued($event);
+ * ```
*/
- public function seeEmailIsSent(int $expectedCount = 1): void
+ public function assertEmailIsQueued(MessageEvent $event, string $message = ''): void
{
- $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($expectedCount));
+ $this->assertThat($event, new MailerConstraint\EmailIsQueued(), $message);
+ }
+
+ /**
+ * Asserts that the expected number of emails was queued (e.g. using the Messenger component).
+ *
+ * ```php
+ * assertQueuedEmailCount(1, 'smtp');
+ * ```
+ */
+ public function assertQueuedEmailCount(int $count, ?string $transport = null, string $message = ''): void
+ {
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport, true), $message);
+ }
+
+ /**
+ * Checks that no email was sent.
+ * The check is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
+ * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first; otherwise this check will *always* pass.
+ *
+ * ```php
+ * dontSeeEmailIsSent();
+ * ```
+ */
+ public function dontSeeEmailIsSent(): void
+ {
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount(0));
}
/**
* Returns the last sent email.
* The function is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
- * If your app performs a HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first.
- * Starting with version 2.0.0, `codeception/module-symfony` requires your app to use [Symfony Mailer](https://symfony.com/doc/current/mailer.html). If your app still uses [Swift Mailer](https://symfony.com/doc/current/email.html), set your version constraint to `^1.6`.
+ * If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first.
* See also: [grabSentEmails()](https://codeception.com/docs/modules/Symfony#grabSentEmails)
*
* ```php
@@ -53,25 +96,20 @@ public function seeEmailIsSent(int $expectedCount = 1): void
* $address = $email->getTo()[0];
* $I->assertSame('john_doe@example.com', $address->getAddress());
* ```
- *
- * @return \Symfony\Component\Mime\Email|null
*/
public function grabLastSentEmail(): ?Email
{
+ /** @var Email[] $emails */
$emails = $this->getMessageMailerEvents()->getMessages();
- /** @var Email|false $lastEmail */
- if ($lastEmail = end($emails)) {
- return $lastEmail;
- }
+ $lastEmail = end($emails);
- return null;
+ return $lastEmail ?: null;
}
/**
* Returns an array of all sent emails.
* The function is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
- * If your app performs a HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first.
- * Starting with version 2.0.0, `codeception/module-symfony` requires your app to use [Symfony Mailer](https://symfony.com/doc/current/mailer.html). If your app still uses [Swift Mailer](https://symfony.com/doc/current/email.html), set your version constraint to `^1.6`.
+ * If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first.
* See also: [grabLastSentEmail()](https://codeception.com/docs/modules/Symfony#grabLastSentEmail)
*
* ```php
@@ -86,21 +124,53 @@ public function grabSentEmails(): array
return $this->getMessageMailerEvents()->getMessages();
}
+ /**
+ * Checks if the given number of emails was sent (default `$expectedCount`: 1).
+ * The check is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
+ * If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first.
+ *
+ * Limitation:
+ * If your mail is sent in a Symfony console command and you start that command in your test with [$I->runShellCommand()](https://codeception.com/docs/modules/Cli#runShellCommand),
+ * Codeception will not notice it.
+ * As a more professional alternative, we recommend Mailpit (see [Addons](https://codeception.com/addons)), which also lets you test the content of the mail.
+ *
+ * ```php
+ * seeEmailIsSent(2);
+ * ```
+ *
+ * @param int $expectedCount The expected number of emails sent
+ */
+ public function seeEmailIsSent(int $expectedCount = 1): void
+ {
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($expectedCount));
+ }
+
+ /**
+ * Returns the mailer event at the specified index.
+ *
+ * ```php
+ * getMailerEvent();
+ * ```
+ */
+ public function getMailerEvent(int $index = 0, ?string $transport = null): ?MessageEvent
+ {
+ $mailerEvents = $this->getMessageMailerEvents();
+ $events = $mailerEvents->getEvents($transport);
+ return $events[$index] ?? null;
+ }
+
protected function getMessageMailerEvents(): MessageEvents
{
- if ($messageLogger = $this->getService('mailer.message_logger_listener')) {
- /** @var MessageLoggerListener $messageLogger */
- return $messageLogger->getEvents();
+ if ($mailer = $this->getService('mailer.message_logger_listener')) {
+ /** @var MessageLoggerListener $mailer */
+ return $mailer->getEvents();
}
-
- if ($messageLogger = $this->getService('mailer.logger_message_listener')) {
- /** @var MessageLoggerListener $messageLogger */
- return $messageLogger->getEvents();
+ if ($mailer = $this->getService('mailer.logger_message_listener')) {
+ /** @var MessageLoggerListener $mailer */
+ return $mailer->getEvents();
}
-
- $this->fail("codeception/module-symfony requires Symfony Mailer https://symfony.com/doc/current/mailer.html to test emails. If your app still uses Swift Mailer, downgrade codeception/module-symfony to ^1.6
-
-
- Emails can't be tested without Symfony Mailer service.");
+ $this->fail("Emails can't be tested without Symfony Mailer service.");
}
}
diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
index 9228602a..d48df3d4 100644
--- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
@@ -4,6 +4,7 @@
namespace Codeception\Module\Symfony;
+use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Test\Constraint as MimeConstraint;
@@ -20,7 +21,7 @@ trait MimeAssertionsTrait
* $I->assertEmailAddressContains('To', 'jane_doe@example.com');
* ```
*/
- public function assertEmailAddressContains(string $headerName, string $expectedValue, Email $email = null): void
+ public function assertEmailAddressContains(string $headerName, string $expectedValue, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue));
@@ -35,7 +36,7 @@ public function assertEmailAddressContains(string $headerName, string $expectedV
* $I->assertEmailAttachmentCount(1);
* ```
*/
- public function assertEmailAttachmentCount(int $count, Email $email = null): void
+ public function assertEmailAttachmentCount(int $count, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailAttachmentCount($count));
@@ -50,7 +51,7 @@ public function assertEmailAttachmentCount(int $count, Email $email = null): voi
* $I->assertEmailHasHeader('Bcc');
* ```
*/
- public function assertEmailHasHeader(string $headerName, Email $email = null): void
+ public function assertEmailHasHeader(string $headerName, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailHasHeader($headerName));
@@ -66,7 +67,7 @@ public function assertEmailHasHeader(string $headerName, Email $email = null): v
* $I->assertEmailHeaderNotSame('To', 'john_doe@gmail.com');
* ```
*/
- public function assertEmailHeaderNotSame(string $headerName, string $expectedValue, Email $email = null): void
+ public function assertEmailHeaderNotSame(string $headerName, string $expectedValue, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)));
@@ -82,7 +83,7 @@ public function assertEmailHeaderNotSame(string $headerName, string $expectedVal
* $I->assertEmailHeaderSame('To', 'jane_doe@gmail.com');
* ```
*/
- public function assertEmailHeaderSame(string $headerName, string $expectedValue, Email $email = null): void
+ public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailHeaderSame($headerName, $expectedValue));
@@ -97,7 +98,7 @@ public function assertEmailHeaderSame(string $headerName, string $expectedValue,
* $I->assertEmailHtmlBodyContains('Successful registration');
* ```
*/
- public function assertEmailHtmlBodyContains(string $text, Email $email = null): void
+ public function assertEmailHtmlBodyContains(string $text, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailHtmlBodyContains($text));
@@ -112,7 +113,7 @@ public function assertEmailHtmlBodyContains(string $text, Email $email = null):
* $I->assertEmailHtmlBodyNotContains('userpassword');
* ```
*/
- public function assertEmailHtmlBodyNotContains(string $text, Email $email = null): void
+ public function assertEmailHtmlBodyNotContains(string $text, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text)));
@@ -127,7 +128,7 @@ public function assertEmailHtmlBodyNotContains(string $text, Email $email = null
* $I->assertEmailNotHasHeader('Bcc');
* ```
*/
- public function assertEmailNotHasHeader(string $headerName, Email $email = null): void
+ public function assertEmailNotHasHeader(string $headerName, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName)));
@@ -142,7 +143,7 @@ public function assertEmailNotHasHeader(string $headerName, Email $email = null)
* $I->assertEmailTextBodyContains('Example text body');
* ```
*/
- public function assertEmailTextBodyContains(string $text, Email $email = null): void
+ public function assertEmailTextBodyContains(string $text, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new MimeConstraint\EmailTextBodyContains($text));
@@ -157,7 +158,7 @@ public function assertEmailTextBodyContains(string $text, Email $email = null):
* $I->assertEmailTextBodyNotContains('My secret text body');
* ```
*/
- public function assertEmailTextBodyNotContains(string $text, Email $email = null): void
+ public function assertEmailTextBodyNotContains(string $text, ?Email $email = null): void
{
$email = $this->verifyEmailObject($email, __FUNCTION__);
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailTextBodyContains($text)));
@@ -169,9 +170,9 @@ public function assertEmailTextBodyNotContains(string $text, Email $email = null
private function verifyEmailObject(?Email $email, string $function): Email
{
$email = $email ?: $this->grabLastSentEmail();
- $errorMsgFormat = "There is no email to verify. An Email object was not specified when invoking '%s' and the application has not sent one.";
- return $email ?: $this->fail(
- sprintf($errorMsgFormat, $function)
+ $errorMsgTemplate = "There is no email to verify. An Email object was not specified when invoking '%s' and the application has not sent one.";
+ return $email ?? Assert::fail(
+ sprintf($errorMsgTemplate, $function)
);
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php
index cb5bcad2..ecbbbbc7 100644
--- a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php
@@ -5,6 +5,7 @@
namespace Codeception\Module\Symfony;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+use UnitEnum;
trait ParameterAssertionsTrait
{
@@ -15,14 +16,12 @@ trait ParameterAssertionsTrait
* grabParameter('app.business_name');
* ```
- *
- * @param string $name
- * @return array|bool|float|int|string|null
+ * This only works for explicitly set parameters (just using `bind` for Symfony's dependency injection is not enough).
*/
- public function grabParameter(string $name)
+ public function grabParameter(string $parameterName): array|bool|string|int|float|UnitEnum|null
{
$parameterBag = $this->grabParameterBagService();
- return $parameterBag->get($name);
+ return $parameterBag->get($parameterName);
}
protected function grabParameterBagService(): ParameterBagInterface
diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
index 4ca8032d..699b23b1 100644
--- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
@@ -5,14 +5,11 @@
namespace Codeception\Module\Symfony;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
-use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
use function array_intersect_assoc;
-use function array_merge;
use function explode;
use function sprintf;
-use function strlen;
-use function substr_compare;
trait RouterAssertionsTrait
{
@@ -25,24 +22,20 @@ trait RouterAssertionsTrait
* $I->amOnAction('HomeController');
* $I->amOnAction('ArticleController', ['slug' => 'lorem-ipsum']);
* ```
- *
- * @param string $action
- * @param array $params
*/
public function amOnAction(string $action, array $params = []): void
{
$router = $this->grabRouterService();
-
$routes = $router->getRouteCollection()->getIterator();
+ /** @var Route $route */
foreach ($routes as $route) {
$controller = $route->getDefault('_controller');
- if (substr_compare($controller, $action, -strlen($action)) === 0) {
+ if (str_ends_with((string) $controller, $action)) {
$resource = $router->match($route->getPath());
$url = $router->generate(
$resource['_route'],
- $params,
- UrlGeneratorInterface::ABSOLUTE_PATH
+ $params
);
$this->amOnPage($url);
return;
@@ -58,15 +51,12 @@ public function amOnAction(string $action, array $params = []): void
* $I->amOnRoute('posts.create');
* $I->amOnRoute('posts.show', ['id' => 34]);
* ```
- *
- * @param string $routeName
- * @param array $params
*/
public function amOnRoute(string $routeName, array $params = []): void
{
$router = $this->grabRouterService();
if ($router->getRouteCollection()->get($routeName) === null) {
- $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
+ $this->fail(sprintf('Route with name "%s" does not exist.', $routeName));
}
$url = $router->generate($routeName, $params);
@@ -89,18 +79,16 @@ public function invalidateCachedRouter(): void
* $I->seeCurrentActionIs('PostController::index');
* $I->seeCurrentActionIs('HomeController');
* ```
- *
- * @param string $action
*/
public function seeCurrentActionIs(string $action): void
{
$router = $this->grabRouterService();
-
$routes = $router->getRouteCollection()->getIterator();
+ /** @var Route $route */
foreach ($routes as $route) {
$controller = $route->getDefault('_controller');
- if (substr_compare($controller, $action, -strlen($action)) === 0) {
+ if (str_ends_with((string) $controller, $action)) {
$request = $this->getClient()->getRequest();
$currentActionFqcn = $request->attributes->get('_controller');
@@ -120,26 +108,24 @@ public function seeCurrentActionIs(string $action): void
* $I->seeCurrentRouteIs('posts.index');
* $I->seeCurrentRouteIs('posts.show', ['id' => 8]);
* ```
- *
- * @param string $routeName
- * @param array $params
*/
public function seeCurrentRouteIs(string $routeName, array $params = []): void
{
$router = $this->grabRouterService();
if ($router->getRouteCollection()->get($routeName) === null) {
- $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
+ $this->fail(sprintf('Route with name "%s" does not exist.', $routeName));
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
+ $uri = explode('#', $uri)[0];
$match = [];
try {
$match = $router->match($uri);
- } catch (ResourceNotFoundException $e) {
+ } catch (ResourceNotFoundException) {
$this->fail(sprintf('The "%s" url does not match with any route', $uri));
}
- $expected = array_merge(['_route' => $routeName], $params);
+ $expected = ['_route' => $routeName, ...$params];
$intersection = array_intersect_assoc($expected, $match);
$this->assertSame($expected, $intersection);
@@ -153,21 +139,20 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void
* seeInCurrentRoute('my_blog_pages');
* ```
- *
- * @param string $routeName
*/
public function seeInCurrentRoute(string $routeName): void
{
$router = $this->grabRouterService();
if ($router->getRouteCollection()->get($routeName) === null) {
- $this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
+ $this->fail(sprintf('Route with name "%s" does not exist.', $routeName));
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
+ $uri = explode('#', $uri)[0];
$matchedRouteName = '';
try {
$matchedRouteName = (string)$router->match($uri)['_route'];
- } catch (ResourceNotFoundException $e) {
+ } catch (ResourceNotFoundException) {
$this->fail(sprintf('The "%s" url does not match with any route', $uri));
}
@@ -178,4 +163,4 @@ protected function grabRouterService(): RouterInterface
{
return $this->grabService('router');
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php
index a8ad24b6..81559730 100644
--- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php
@@ -4,10 +4,11 @@
namespace Codeception\Module\Symfony;
+use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
-use Symfony\Component\Security\Core\Security;
+use Symfony\Component\Security\Core\Security as LegacySecurity;
use Symfony\Component\Security\Core\User\UserInterface;
use function sprintf;
@@ -65,7 +66,7 @@ public function seeAuthentication(): void
{
$security = $this->grabSecurityService();
- if (!$user = $security->getUser()) {
+ if (!$security->getUser()) {
$this->fail('There is no user in session');
}
@@ -108,8 +109,6 @@ public function seeRememberedAuthentication(): void
* seeUserHasRole('ROLE_ADMIN');
* ```
- *
- * @param string $role
*/
public function seeUserHasRole(string $role): void
{
@@ -164,7 +163,7 @@ public function seeUserHasRoles(array $roles): void
*
* @param UserInterface|null $user
*/
- public function seeUserPasswordDoesNotNeedRehash(UserInterface $user = null): void
+ public function seeUserPasswordDoesNotNeedRehash(?UserInterface $user = null): void
{
if ($user === null) {
$security = $this->grabSecurityService();
@@ -178,15 +177,12 @@ public function seeUserPasswordDoesNotNeedRehash(UserInterface $user = null): vo
$this->assertFalse($hasher->needsRehash($user), 'User password needs rehash');
}
- protected function grabSecurityService(): Security
+ protected function grabSecurityService(): Security|LegacySecurity
{
return $this->grabService('security.helper');
}
- /**
- * @return UserPasswordHasherInterface|UserPasswordEncoderInterface
- */
- protected function grabPasswordHasherService()
+ protected function grabPasswordHasherService(): UserPasswordHasherInterface|UserPasswordEncoderInterface
{
$hasher = $this->getService('security.password_hasher') ?: $this->getService('security.password_encoder');
diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php
index c21289c5..1286e252 100644
--- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php
@@ -5,14 +5,15 @@
namespace Codeception\Module\Symfony;
use Codeception\Lib\Connector\Symfony as SymfonyConnector;
+use PHPUnit\Framework\Assert;
trait ServicesAssertionsTrait
{
/**
* Grabs a service from the Symfony dependency injection container (DIC).
- * In "test" environment, Symfony uses a special `test.service_container`.
+ * In the "test" environment, Symfony uses a special `test.service_container`.
* See the "[Public Versus Private Services](https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private)" documentation.
- * Services that aren't injected somewhere into your app, need to be defined as `public` to be accessible by Codeception.
+ * Services that aren't injected anywhere in your app, need to be defined as `public` to be accessible by Codeception.
*
* ```php
* getService($serviceId)) {
- $this->fail("Service `{$serviceId}` is required by Codeception, but not loaded by Symfony since you're not using it anywhere in your app.\n
- Recommended solution: Set it to `public` in your `config/services_test.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private");
+ Assert::fail("Service `{$serviceId}` is required by Codeception, but not loaded by Symfony. Possible solutions:\n
+ In your `config/packages/framework.php`/`.yaml`, set `test` to `true` (when in test environment), see https://symfony.com/doc/current/reference/configuration/framework.html#test\n
+ If you're still getting this message, you're not using that service in your app, so Symfony isn't loading it at all.\n
+ Solution: Set it to `public` in your `config/services.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private\n");
}
return $service;
@@ -37,7 +38,6 @@ public function grabService(string $serviceId): object
* Get service $serviceName and add it to the lists of persistent services.
*
* @part services
- * @param string $serviceName
*/
public function persistService(string $serviceName): void
{
@@ -53,7 +53,6 @@ public function persistService(string $serviceName): void
* making that service persistent between tests.
*
* @part services
- * @param string $serviceName
*/
public function persistPermanentService(string $serviceName): void
{
@@ -69,19 +68,13 @@ public function persistPermanentService(string $serviceName): void
* Remove service $serviceName from the lists of persistent services.
*
* @part services
- * @param string $serviceName
*/
public function unpersistService(string $serviceName): void
{
- if (isset($this->persistentServices[$serviceName])) {
- unset($this->persistentServices[$serviceName]);
- }
-
- if (isset($this->permanentServices[$serviceName])) {
- unset($this->permanentServices[$serviceName]);
- }
+ unset($this->persistentServices[$serviceName]);
+ unset($this->permanentServices[$serviceName]);
- if ($this->client instanceof SymfonyConnector && isset($this->client->persistentServices[$serviceName])) {
+ if ($this->client instanceof SymfonyConnector) {
unset($this->client->persistentServices[$serviceName]);
}
}
diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php
index e7ff9cbb..aa7ac9e9 100644
--- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php
@@ -7,9 +7,14 @@
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
+use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
+use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
+use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
+use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use function is_int;
@@ -29,37 +34,25 @@ trait SessionAssertionsTrait
* ]);
* $I->amLoggedInAs($user);
* ```
- *
- * @param UserInterface $user
- * @param string $firewallName
- * @param null $firewallContext
*/
- public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', $firewallContext = null): void
+ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', ?string $firewallContext = null): void
{
- $session = $this->getCurrentSession();
+ $token = $this->createAuthenticationToken($user, $firewallName);
+ $this->loginWithToken($token, $firewallName, $firewallContext);
+ }
- if ($this->getSymfonyMajorVersion() < 6) {
- if ($this->config['guard']) {
- $token = new PostAuthenticationGuardToken($user, $firewallName, $user->getRoles());
- } else {
- $token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
- }
- } else {
- if ($this->config['authenticator']) {
- $token = new PostAuthenticationToken($user, $firewallName, $user->getRoles());
- } else {
- $token = new UsernamePasswordToken($user, $firewallName, $user->getRoles());
- }
- }
+ public function amLoggedInWithToken(TokenInterface $token, string $firewallName = 'main', ?string $firewallContext = null): void
+ {
+ $this->loginWithToken($token, $firewallName, $firewallContext);
+ }
+ protected function loginWithToken(TokenInterface $token, string $firewallName, ?string $firewallContext): void
+ {
$this->getTokenStorage()->setToken($token);
- if ($firewallContext) {
- $session->set('_security_' . $firewallContext, serialize($token));
- } else {
- $session->set('_security_' . $firewallName, serialize($token));
- }
-
+ $session = $this->getCurrentSession();
+ $sessionKey = $firewallContext ? "_security_{$firewallContext}" : "_security_{$firewallName}";
+ $session->set($sessionKey, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
@@ -74,18 +67,13 @@ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main',
* $I->dontSeeInSession('attribute');
* $I->dontSeeInSession('attribute', 'value');
* ```
- *
- * @param string $attribute
- * @param mixed|null $value
*/
- public function dontSeeInSession(string $attribute, $value = null): void
+ public function dontSeeInSession(string $attribute, mixed $value = null): void
{
$session = $this->getCurrentSession();
- if ($attributeExists = $session->has($attribute)) {
- $this->fail("Session attribute with name '{$attribute}' does exist");
- }
- $this->assertFalse($attributeExists);
+ $attributeExists = $session->has($attribute);
+ $this->assertFalse($attributeExists, "Session attribute '{$attribute}' exists.");
if (null !== $value) {
$this->assertNotSame($value, $session->get($attribute));
@@ -100,8 +88,7 @@ public function dontSeeInSession(string $attribute, $value = null): void
*/
public function goToLogoutPath(): void
{
- $logoutUrlGenerator = $this->getLogoutUrlGenerator();
- $logoutPath = $logoutUrlGenerator->getLogoutPath();
+ $logoutPath = $this->getLogoutUrlGenerator()->getLogoutPath();
$this->amOnPage($logoutPath);
}
@@ -134,17 +121,14 @@ public function logoutProgrammatically(): void
}
$session = $this->getCurrentSession();
-
$sessionName = $session->getName();
$session->invalidate();
$cookieJar = $this->client->getCookieJar();
+ $cookiesToExpire = ['MOCKSESSID', 'REMEMBERME', $sessionName];
foreach ($cookieJar->all() as $cookie) {
$cookieName = $cookie->getName();
- if ($cookieName === 'MOCKSESSID' ||
- $cookieName === 'REMEMBERME' ||
- $cookieName === $sessionName
- ) {
+ if (in_array($cookieName, $cookiesToExpire, true)) {
$cookieJar->expire($cookieName);
}
}
@@ -160,18 +144,13 @@ public function logoutProgrammatically(): void
* $I->seeInSession('attribute');
* $I->seeInSession('attribute', 'value');
* ```
- *
- * @param string $attribute
- * @param mixed|null $value
*/
- public function seeInSession(string $attribute, $value = null): void
+ public function seeInSession(string $attribute, mixed $value = null): void
{
$session = $this->getCurrentSession();
- if (!$attributeExists = $session->has($attribute)) {
- $this->fail("No session attribute with name '{$attribute}'");
- }
- $this->assertTrue($attributeExists);
+ $attributeExists = $session->has($attribute);
+ $this->assertTrue($attributeExists, "No session attribute with name '{$attribute}'");
if (null !== $value) {
$this->assertSame($value, $session->get($attribute));
@@ -186,8 +165,6 @@ public function seeInSession(string $attribute, $value = null): void
* $I->seeSessionHasValues(['key1', 'key2']);
* $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']);
* ```
- *
- * @param array $bindings
*/
public function seeSessionHasValues(array $bindings): void
{
@@ -210,15 +187,16 @@ protected function getLogoutUrlGenerator(): ?LogoutUrlGenerator
return $this->getService('security.logout_url_generator');
}
+ protected function getAuthenticator(): ?AuthenticatorInterface
+ {
+ return $this->getService(AuthenticatorInterface::class);
+ }
+
protected function getCurrentSession(): SessionInterface
{
$container = $this->_getContainer();
- if ($this->getSymfonyMajorVersion() < 6) {
- return $container->get('session');
- }
-
- if ($container->has('session')) {
+ if ($this->getSymfonyMajorVersion() < 6 || $container->has('session')) {
return $container->get('session');
}
@@ -232,4 +210,26 @@ protected function getSymfonyMajorVersion(): int
{
return $this->kernel::MAJOR_VERSION;
}
+
+ /**
+ * @return TokenInterface|GuardTokenInterface
+ */
+ protected function createAuthenticationToken(UserInterface $user, string $firewallName)
+ {
+ $roles = $user->getRoles();
+ if ($this->getSymfonyMajorVersion() < 6) {
+ return $this->config['guard']
+ ? new PostAuthenticationGuardToken($user, $firewallName, $roles)
+ : new UsernamePasswordToken($user, null, $firewallName, $roles);
+ }
+
+ if ($this->config['authenticator']) {
+ if ($authenticator = $this->getAuthenticator()) {
+ $passport = new SelfValidatingPassport(new UserBadge($user->getUserIdentifier(), fn () => $user));
+ return $authenticator->createToken($passport, $firewallName);
+ }
+ return new PostAuthenticationToken($user, $firewallName, $roles);
+ }
+ return new UsernamePasswordToken($user, $firewallName, $roles);
+ }
}
diff --git a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php
index ad3c1862..a1067f37 100644
--- a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php
@@ -13,7 +13,7 @@ trait TimeAssertionsTrait
/**
* Asserts that the time a request lasted is less than expected.
*
- * If the page performed a HTTP redirect, only the time of the last request will be taken into account.
+ * If the page performed an HTTP redirect, only the time of the last request will be taken into account.
* You can modify this behavior using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first.
*
* Also, note that using code coverage can significantly increase the time it takes to resolve a request,
@@ -24,7 +24,7 @@ trait TimeAssertionsTrait
*
* @param int|float $expectedMilliseconds The expected time in milliseconds
*/
- public function seeRequestTimeIsLessThan($expectedMilliseconds): void
+ public function seeRequestTimeIsLessThan(int|float $expectedMilliseconds): void
{
$expectedMilliseconds = round($expectedMilliseconds, 2);
@@ -36,7 +36,7 @@ public function seeRequestTimeIsLessThan($expectedMilliseconds): void
$expectedMilliseconds,
$actualMilliseconds,
sprintf(
- 'The request was expected to last less than %d ms, but it actually lasted %d ms.',
+ 'The request duration was expected to be less than %d ms, but it was actually %d ms.',
$expectedMilliseconds,
$actualMilliseconds
)
diff --git a/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php
new file mode 100644
index 00000000..5fa91725
--- /dev/null
+++ b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php
@@ -0,0 +1,178 @@
+dontSeeFallbackTranslations();
+ * ```
+ */
+ public function dontSeeFallbackTranslations(): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $fallbacks = $translationCollector->getCountFallbacks();
+
+ $this->assertSame(
+ $fallbacks,
+ 0,
+ "Expected no fallback translations, but found {$fallbacks}."
+ );
+ }
+
+ /**
+ * Asserts that no missing translations were found.
+ *
+ * ```php
+ * dontSeeMissingTranslations();
+ * ```
+ */
+ public function dontSeeMissingTranslations(): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $missings = $translationCollector->getCountMissings();
+
+ $this->assertSame(
+ $missings,
+ 0,
+ "Expected no missing translations, but found {$missings}."
+ );
+ }
+
+ /**
+ * Grabs the count of defined translations.
+ *
+ * ```php
+ * grabDefinedTranslations();
+ * ```
+ *
+ * @return int The count of defined translations.
+ */
+ public function grabDefinedTranslationsCount(): int
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ return $translationCollector->getCountDefines();
+ }
+
+ /**
+ * Asserts that there are no missing translations and no fallback translations.
+ *
+ * ```php
+ * seeAllTranslationsDefined();
+ * ```
+ */
+ public function seeAllTranslationsDefined(): void
+ {
+ $this->dontSeeMissingTranslations();
+ $this->dontSeeFallbackTranslations();
+ }
+
+ /**
+ * Asserts that the default locale is the expected one.
+ *
+ * ```php
+ * seeDefaultLocaleIs('en');
+ * ```
+ *
+ * @param string $expectedLocale The expected default locale
+ */
+ public function seeDefaultLocaleIs(string $expectedLocale): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $locale = $translationCollector->getLocale();
+
+ $this->assertSame(
+ $expectedLocale,
+ $locale,
+ "Expected default locale '{$expectedLocale}', but found '{$locale}'."
+ );
+ }
+
+ /**
+ * Asserts that the fallback locales match the expected ones.
+ *
+ * ```php
+ * seeFallbackLocalesAre(['es', 'fr']);
+ * ```
+ *
+ * @param string[] $expectedLocales The expected fallback locales
+ */
+ public function seeFallbackLocalesAre(array $expectedLocales): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $fallbackLocales = $translationCollector->getFallbackLocales();
+
+ if ($fallbackLocales instanceof Data) {
+ $fallbackLocales = $fallbackLocales->getValue(true);
+ }
+
+ $this->assertSame(
+ $expectedLocales,
+ $fallbackLocales,
+ "Fallback locales do not match expected."
+ );
+ }
+
+ /**
+ * Asserts that the count of fallback translations is less than the given limit.
+ *
+ * ```php
+ * seeFallbackTranslationsCountLessThan(10);
+ * ```
+ *
+ * @param int $limit Maximum count of fallback translations
+ */
+ public function seeFallbackTranslationsCountLessThan(int $limit): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $fallbacks = $translationCollector->getCountFallbacks();
+
+ $this->assertLessThan(
+ $limit,
+ $fallbacks,
+ "Expected fewer than {$limit} fallback translations, but found {$fallbacks}."
+ );
+ }
+
+ /**
+ * Asserts that the count of missing translations is less than the given limit.
+ *
+ * ```php
+ * seeMissingTranslationsCountLessThan(5);
+ * ```
+ *
+ * @param int $limit Maximum count of missing translations
+ */
+ public function seeMissingTranslationsCountLessThan(int $limit): void
+ {
+ $translationCollector = $this->grabTranslationCollector(__FUNCTION__);
+ $missings = $translationCollector->getCountMissings();
+
+ $this->assertLessThan(
+ $limit,
+ $missings,
+ "Expected fewer than {$limit} missing translations, but found {$missings}."
+ );
+ }
+
+ protected function grabTranslationCollector(string $function): TranslationDataCollector
+ {
+ return $this->grabCollector('translation', $function);
+ }
+}
diff --git a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php
index 624b822e..e664932c 100644
--- a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php
@@ -16,14 +16,12 @@ trait TwigAssertionsTrait
* dontSeeRenderedTemplate('home.html.twig');
* ```
- *
- * @param string $template
*/
public function dontSeeRenderedTemplate(string $template): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
+ $templates = $twigCollector->getTemplates();
$this->assertArrayNotHasKey(
$template,
@@ -39,15 +37,13 @@ public function dontSeeRenderedTemplate(string $template): void
* seeCurrentTemplateIs('home.html.twig');
* ```
- *
- * @param string $expectedTemplate
*/
public function seeCurrentTemplateIs(string $expectedTemplate): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
- $actualTemplate = (string)array_key_first($templates);
+ $templates = $twigCollector->getTemplates();
+ $actualTemplate = empty($templates) ? 'N/A' : (string) array_key_first($templates);
$this->assertSame(
$expectedTemplate,
@@ -65,14 +61,12 @@ public function seeCurrentTemplateIs(string $expectedTemplate): void
* $I->seeRenderedTemplate('home.html.twig');
* $I->seeRenderedTemplate('layout.html.twig');
* ```
- *
- * @param string $template
*/
public function seeRenderedTemplate(string $template): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
+ $templates = $twigCollector->getTemplates();
$this->assertArrayHasKey(
$template,
@@ -85,4 +79,4 @@ protected function grabTwigCollector(string $function): TwigDataCollector
{
return $this->grabCollector('twig', $function);
}
-}
\ No newline at end of file
+}
diff --git a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php
new file mode 100644
index 00000000..508cfa5e
--- /dev/null
+++ b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php
@@ -0,0 +1,106 @@
+dontSeeViolatedConstraint($subject);
+ * $I->dontSeeViolatedConstraint($subject, 'propertyName');
+ * $I->dontSeeViolatedConstraint($subject, 'propertyName', 'Symfony\Validator\ConstraintClass');
+ * ```
+ */
+ public function dontSeeViolatedConstraint(object $subject, ?string $propertyPath = null, ?string $constraint = null): void
+ {
+ $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint);
+ $this->assertCount(0, $violations, 'Constraint violations found.');
+ }
+
+ /**
+ * Asserts that the given subject passes validation.
+ * This assertion does not concern the exact number of violations.
+ *
+ * ```php
+ * seeViolatedConstraint($subject);
+ * $I->seeViolatedConstraint($subject, 'propertyName');
+ * $I->seeViolatedConstraint($subject, 'propertyName', 'Symfony\Validator\ConstraintClass');
+ * ```
+ */
+ public function seeViolatedConstraint(object $subject, ?string $propertyPath = null, ?string $constraint = null): void
+ {
+ $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint);
+ $this->assertNotCount(0, $violations, 'No constraint violations found.');
+ }
+
+ /**
+ * Asserts the exact number of violations for the given subject.
+ *
+ * ```php
+ * seeViolatedConstraintsCount(3, $subject);
+ * $I->seeViolatedConstraintsCount(2, $subject, 'propertyName');
+ * ```
+ */
+ public function seeViolatedConstraintsCount(int $expected, object $subject, ?string $propertyPath = null, ?string $constraint = null): void
+ {
+ $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint);
+ $this->assertCount($expected, $violations);
+ }
+
+ /**
+ * Asserts that a constraint violation message or a part of it is present in the subject's violations.
+ *
+ * ```php
+ * seeViolatedConstraintMessage('too short', $user, 'address');
+ * ```
+ */
+ public function seeViolatedConstraintMessage(string $expected, object $subject, string $propertyPath): void
+ {
+ $violations = $this->getViolationsForSubject($subject, $propertyPath);
+ $containsExpected = false;
+ foreach ($violations as $violation) {
+ if ($violation->getPropertyPath() === $propertyPath && str_contains((string)$violation->getMessage(), $expected)) {
+ $containsExpected = true;
+ break;
+ }
+ }
+
+ $this->assertTrue($containsExpected, 'The violation messages do not contain: ' . $expected);
+ }
+
+ /** @return ConstraintViolationInterface[] */
+ protected function getViolationsForSubject(object $subject, ?string $propertyPath = null, ?string $constraint = null): array
+ {
+ $validator = $this->getValidatorService();
+ $violations = $propertyPath ? $validator->validateProperty($subject, $propertyPath) : $validator->validate($subject);
+
+ $violations = iterator_to_array($violations);
+
+ if ($constraint !== null) {
+ return (array)array_filter(
+ $violations,
+ static fn(ConstraintViolationInterface $violation): bool => get_class((object)$violation->getConstraint()) === $constraint &&
+ ($propertyPath === null || $violation->getPropertyPath() === $propertyPath)
+ );
+ }
+
+ return $violations;
+ }
+
+ protected function getValidatorService(): ValidatorInterface
+ {
+ return $this->grabService(ValidatorInterface::class);
+ }
+}
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy