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/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 0ef1ee4c..f761499b 100644
--- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php
@@ -23,9 +23,9 @@ trait EventsAssertionsTrait
*
* @param string|string[]|null $expected
*/
- public function dontSeeEvent(array|string $expected = null): void
+ public function dontSeeEvent(array|string|null $expected = null): void
{
- $actualEvents = array_merge(array_column($this->getCalledListeners(), 'event'));
+ $actualEvents = [...array_column($this->getCalledListeners(), 'event')];
$actual = [$this->getOrphanedEvents(), $actualEvents];
$this->assertEventTriggered(false, $expected, $actual);
}
@@ -87,7 +87,7 @@ public function dontSeeEventTriggered(array|object|string $expected): void
*
* @param string|string[] $expected
*/
- public function dontSeeOrphanEvent(array|string $expected = null): void
+ public function dontSeeOrphanEvent(array|string|null $expected = null): void
{
$actual = [$this->getOrphanedEvents()];
$this->assertEventTriggered(false, $expected, $actual);
@@ -110,7 +110,7 @@ public function dontSeeOrphanEvent(array|string $expected = null): void
*/
public function seeEvent(array|string $expected): void
{
- $actualEvents = array_merge(array_column($this->getCalledListeners(), 'event'));
+ $actualEvents = [...array_column($this->getCalledListeners(), 'event')];
$actual = [$this->getOrphanedEvents(), $actualEvents];
$this->assertEventTriggered(true, $expected, $actual);
}
diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php
index 31940e15..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,11 +75,8 @@ 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
+ public function seeFormErrorMessage(string $field, ?string $message = null): void
{
$formCollector = $this->grabFormCollector(__FUNCTION__);
@@ -108,7 +138,6 @@ public function seeFormErrorMessage(string $field, string $message = null): void
* 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): void
* ```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 4e61cb2d..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
@@ -66,8 +109,7 @@ public function grabLastSentEmail(): ?Email
/**
* 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
@@ -82,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 63231dd5..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,11 +16,9 @@ trait ParameterAssertionsTrait
* grabParameter('app.business_name');
* ```
- *
- * @param string $parameterName
- * @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 $parameterName)
+ public function grabParameter(string $parameterName): array|bool|string|int|float|UnitEnum|null
{
$parameterBag = $this->grabParameterBagService();
return $parameterBag->get($parameterName);
diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
index e0bdeab0..699b23b1 100644
--- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php
@@ -5,10 +5,9 @@
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;
@@ -23,23 +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 (str_ends_with($controller, $action)) {
+ 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;
@@ -55,9 +51,6 @@ 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
{
@@ -86,17 +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 (str_ends_with($controller, $action)) {
+ if (str_ends_with((string) $controller, $action)) {
$request = $this->getClient()->getRequest();
$currentActionFqcn = $request->attributes->get('_controller');
@@ -116,9 +108,6 @@ 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
{
@@ -128,6 +117,7 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
+ $uri = explode('#', $uri)[0];
$match = [];
try {
$match = $router->match($uri);
@@ -135,7 +125,7 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void
$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);
@@ -149,8 +139,6 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void
* seeInCurrentRoute('my_blog_pages');
* ```
- *
- * @param string $routeName
*/
public function seeInCurrentRoute(string $routeName): void
{
@@ -160,6 +148,7 @@ public function seeInCurrentRoute(string $routeName): void
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
+ $uri = explode('#', $uri)[0];
$matchedRouteName = '';
try {
$matchedRouteName = (string)$router->match($uri)['_route'];
@@ -174,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 a07e3ab3..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,7 +177,7 @@ 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');
}
diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php
index bd9140c0..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;
diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php
index a8b69afd..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;
@@ -30,14 +35,22 @@ trait SessionAssertionsTrait
* $I->amLoggedInAs($user);
* ```
*/
- public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', string $firewallContext = null): void
+ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', ?string $firewallContext = null): void
{
- $session = $this->getCurrentSession();
- $roles = $user->getRoles();
+ $token = $this->createAuthenticationToken($user, $firewallName);
+ $this->loginWithToken($token, $firewallName, $firewallContext);
+ }
- $token = $this->createAuthenticationToken($user, $firewallName, $roles);
+ 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);
+ $session = $this->getCurrentSession();
$sessionKey = $firewallContext ? "_security_{$firewallContext}" : "_security_{$firewallName}";
$session->set($sessionKey, serialize($token));
$session->save();
@@ -174,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');
}
@@ -198,18 +212,24 @@ protected function getSymfonyMajorVersion(): int
}
/**
- * @return UsernamePasswordToken|PostAuthenticationGuardToken|PostAuthenticationToken
+ * @return TokenInterface|GuardTokenInterface
*/
- protected function createAuthenticationToken(UserInterface $user, string $firewallName, array $roles)
+ 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);
}
- return $this->config['authenticator']
- ? new PostAuthenticationToken($user, $firewallName, $roles)
- : new UsernamePasswordToken($user, $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 136bef25..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,
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 52b02d0d..e664932c 100644
--- a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php
@@ -21,7 +21,7 @@ public function dontSeeRenderedTemplate(string $template): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
+ $templates = $twigCollector->getTemplates();
$this->assertArrayNotHasKey(
$template,
@@ -42,8 +42,8 @@ public function seeCurrentTemplateIs(string $expectedTemplate): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
- $actualTemplate = !empty($templates) ? (string) array_key_first($templates) : 'N/A';
+ $templates = $twigCollector->getTemplates();
+ $actualTemplate = empty($templates) ? 'N/A' : (string) array_key_first($templates);
$this->assertSame(
$expectedTemplate,
@@ -66,7 +66,7 @@ public function seeRenderedTemplate(string $template): void
{
$twigCollector = $this->grabTwigCollector(__FUNCTION__);
- $templates = (array)$twigCollector->getTemplates();
+ $templates = $twigCollector->getTemplates();
$this->assertArrayHasKey(
$template,
@@ -79,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