diff --git a/composer.json b/composer.json
index 684c23c..0bdc3bd 100644
--- a/composer.json
+++ b/composer.json
@@ -5,6 +5,7 @@
"type": "library",
"keywords": [
"codeception",
+ "functional testing",
"symfony"
],
"authors": [
@@ -37,10 +38,12 @@
"symfony/filesystem": "^5.4 | ^6.4 | ^7.0",
"symfony/form": "^5.4 | ^6.4 | ^7.0",
"symfony/framework-bundle": "^5.4 | ^6.4 | ^7.0",
+ "symfony/http-client": "^5.4 | ^6.4 | ^7.0",
"symfony/http-foundation": "^5.4 | ^6.4 | ^7.0",
"symfony/http-kernel": "^5.4 | ^6.4 | ^7.0",
"symfony/mailer": "^5.4 | ^6.4 | ^7.0",
"symfony/mime": "^5.4 | ^6.4 | ^7.0",
+ "symfony/notifier": "5.4 | ^6.4 | ^7.0",
"symfony/options-resolver": "^5.4 | ^6.4 | ^7.0",
"symfony/property-access": "^5.4 | ^6.4 | ^7.0",
"symfony/property-info": "^5.4 | ^6.4 | ^7.0",
diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php
index c508d5e..3469489 100644
--- a/src/Codeception/Module/Symfony.php
+++ b/src/Codeception/Module/Symfony.php
@@ -13,10 +13,13 @@
use Codeception\Module\Symfony\BrowserAssertionsTrait;
use Codeception\Module\Symfony\ConsoleAssertionsTrait;
use Codeception\Module\Symfony\DoctrineAssertionsTrait;
+use Codeception\Module\Symfony\DomCrawlerAssertionsTrait;
use Codeception\Module\Symfony\EventsAssertionsTrait;
use Codeception\Module\Symfony\FormAssertionsTrait;
+use Codeception\Module\Symfony\HttpClientAssertionsTrait;
use Codeception\Module\Symfony\MailerAssertionsTrait;
use Codeception\Module\Symfony\MimeAssertionsTrait;
+use Codeception\Module\Symfony\NotificationAssertionsTrait;
use Codeception\Module\Symfony\ParameterAssertionsTrait;
use Codeception\Module\Symfony\RouterAssertionsTrait;
use Codeception\Module\Symfony\SecurityAssertionsTrait;
@@ -135,10 +138,13 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule
use BrowserAssertionsTrait;
use ConsoleAssertionsTrait;
use DoctrineAssertionsTrait;
+ use DomCrawlerAssertionsTrait;
use EventsAssertionsTrait;
use FormAssertionsTrait;
+ use HttpClientAssertionsTrait;
use MailerAssertionsTrait;
use MimeAssertionsTrait;
+ use NotificationAssertionsTrait;
use ParameterAssertionsTrait;
use RouterAssertionsTrait;
use SecurityAssertionsTrait;
diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
index 9eb5636..d9215bc 100644
--- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
@@ -4,11 +4,191 @@
namespace Codeception\Module\Symfony;
+use PHPUnit\Framework\Constraint\Constraint;
+use PHPUnit\Framework\Constraint\LogicalAnd;
+use PHPUnit\Framework\Constraint\LogicalNot;
+use Symfony\Component\BrowserKit\Test\Constraint\BrowserCookieValueSame;
+use Symfony\Component\BrowserKit\Test\Constraint\BrowserHasCookie;
+use Symfony\Component\HttpFoundation\Test\Constraint\RequestAttributeValueSame;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseCookieValueSame;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseFormatSame;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHasCookie;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHasHeader;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderSame;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsRedirected;
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsSuccessful;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsUnprocessable;
+use Symfony\Component\HttpFoundation\Test\Constraint\ResponseStatusCodeSame;
use function sprintf;
trait BrowserAssertionsTrait
{
+ /**
+ * Asserts the response format returned by the `Response::getFormat()` method is the same as the expected value.
+ */
+ public function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseFormatSame($this->getClient()->getRequest(), $expectedFormat), $message);
+ }
+
+ /**
+ * Asserts a specific HTTP status code.
+ */
+ public function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseStatusCodeSame($expectedCode, $verbose), $message);
+ }
+
+ /**
+ * Asserts the response is a redirect response (optionally, you can check the target location and status code).
+ * The excepted location can be either an absolute or a relative path.
+ */
+ public function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true): void
+ {
+ $constraint = new ResponseIsRedirected($verbose);
+ if ($expectedLocation) {
+ if (class_exists(ResponseHeaderLocationSame::class)) {
+ $locationConstraint = new ResponseHeaderLocationSame($this->getClient()->getRequest(), $expectedLocation);
+ } else {
+ $locationConstraint = new ResponseHeaderSame('Location', $expectedLocation);
+ }
+
+ $constraint = LogicalAnd::fromConstraints($constraint, $locationConstraint);
+ }
+ if ($expectedCode) {
+ $constraint = LogicalAnd::fromConstraints($constraint, new ResponseStatusCodeSame($expectedCode));
+ }
+
+ $this->assertThatForResponse($constraint, $message);
+ }
+
+ /**
+ * Asserts the given header is available on the response, e.g. assertResponseHasHeader('content-type');.
+ */
+ public function assertResponseHasHeader(string $headerName, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHasHeader($headerName), $message);
+ }
+
+ /**
+ * Asserts the given header is not available on the response, e.g. assertResponseNotHasHeader('content-type');.
+ */
+ public function assertResponseNotHasHeader(string $headerName, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHasHeader($headerName)), $message);
+ }
+
+ /**
+ * Asserts the given header does contain the expected value on the response,
+ * e.g. assertResponseHeaderSame('content-type', 'application/octet-stream');.
+ */
+ public function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHeaderSame($headerName, $expectedValue), $message);
+ }
+
+ /**
+ * Asserts the given header does not contain the expected value on the response,
+ * e.g. assertResponseHeaderNotSame('content-type', 'application/octet-stream');.
+ */
+ public function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHeaderSame($headerName, $expectedValue)), $message);
+ }
+
+ /**
+ * Asserts the given cookie is present in the response (optionally checking for a specific cookie path or domain).
+ */
+ public function assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHasCookie($name, $path, $domain), $message);
+ }
+
+ /**
+ * Asserts the given cookie is not present in the response (optionally checking for a specific cookie path or domain).
+ */
+ public function assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHasCookie($name, $path, $domain)), $message);
+ }
+
+ /**
+ * Asserts the given cookie is present and set to the expected value.
+ */
+ public function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(LogicalAnd::fromConstraints(
+ new ResponseHasCookie($name, $path, $domain),
+ new ResponseCookieValueSame($name, $expectedValue, $path, $domain)
+ ), $message);
+ }
+
+ /**
+ * Asserts the response is unprocessable (HTTP status is 422)
+ */
+ public function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseIsUnprocessable($verbose), $message);
+ }
+
+ /**
+ * Asserts that the test Client does have the given cookie set (meaning, the cookie was set by any response in the test).
+ */
+ public function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForClient(new BrowserHasCookie($name, $path, $domain), $message);
+ }
+
+ protected function assertThatForClient(Constraint $constraint, string $message = ''): void
+ {
+ $this->assertThat($this->getClient(), $constraint, $message);
+ }
+
+ /**
+ * Asserts that the test Client does not have the given cookie set (meaning, the cookie was set by any response in the test).
+ */
+ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForClient(new LogicalNot(new BrowserHasCookie($name, $path, $domain)), $message);
+ }
+
+ /**
+ * Asserts the given cookie in the test Client is set to the expected value.
+ */
+ public function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForClient(LogicalAnd::fromConstraints(
+ new BrowserHasCookie($name, $path, $domain),
+ new BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain)
+ ), $message);
+ }
+
+ /**
+ * Asserts the given request attribute is set to the expected value.
+ */
+ public function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($this->getClient()->getRequest(), new RequestAttributeValueSame($name, $expectedValue), $message);
+ }
+
+ /**
+ * Asserts the request matches the given route and optionally route parameters.
+ */
+ public function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void
+ {
+ $constraint = new RequestAttributeValueSame('_route', $expectedRoute);
+ $constraints = [];
+ foreach ($parameters as $key => $value) {
+ $constraints[] = new RequestAttributeValueSame($key, $value);
+ }
+ if ($constraints) {
+ $constraint = LogicalAnd::fromConstraints($constraint, ...$constraints);
+ }
+
+ $this->assertThat($this->getClient()->getRequest(), $constraint, $message);
+ }
+
/**
* Reboot client's kernel.
* Can be used to manually reboot kernel when 'rebootable_client' => false
@@ -50,7 +230,15 @@ public function seePageIsAvailable(?string $url = null): void
$this->seeInCurrentUrl($url);
}
- $this->assertThat($this->getClient()->getResponse(), new ResponseIsSuccessful());
+ $this->assertResponseIsSuccessful();
+ }
+
+ /**
+ * Asserts that the response was successful (HTTP status is 2xx).
+ */
+ public function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseIsSuccessful($verbose), $message);
}
/**
@@ -104,4 +292,9 @@ public function submitSymfonyForm(string $name, array $fields): void
$this->submitForm($selector, $params, $button);
}
+
+ protected function assertThatForResponse(Constraint $constraint, string $message = ''): void
+ {
+ $this->assertThat($this->getClient()->getResponse(), $constraint, $message);
+ }
}
diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php
new file mode 100644
index 0000000..4f5627e
--- /dev/null
+++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php
@@ -0,0 +1,176 @@
+assertThat($this->getCrawler(), new CrawlerSelectorExists($selector), $message);
+ }
+
+ /**
+ * Asserts that the given selector does not match at least one element in the response.
+ */
+ public function assertSelectorNotExists(string $selector, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), new LogicalNot(new CrawlerSelectorExists($selector)), $message);
+ }
+
+ /**
+ * Asserts that the expected number of selector elements are in the response.
+ */
+ public function assertSelectorCount(int $expectedCount, string $selector, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), new CrawlerSelectorCount($expectedCount, $selector), $message);
+ }
+
+ /**
+ * Asserts that any element matching the given selector does contain the expected text.
+ */
+ public function assertAnySelectorTextContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new CrawlerAnySelectorTextContains($selector, $text)
+ ), $message);
+ }
+
+ /**
+ * Asserts that any element matching the given selector does equal the expected text.
+ */
+ public function assertAnySelectorTextSame(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new CrawlerAnySelectorTextSame($selector, $text)
+ ), $message);
+ }
+
+ /**
+ * Asserts that the first element matching the given selector does not contain the expected text.
+ */
+ public function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new LogicalNot(new CrawlerSelectorTextContains($selector, $text))
+ ), $message);
+ }
+
+ /**
+ * Asserts that any element matching the given selector does not contain the expected text.
+ */
+ public function assertAnySelectorTextNotContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new LogicalNot(new CrawlerAnySelectorTextContains($selector, $text))
+ ), $message);
+ }
+
+ /**
+ * Asserts that the `
` element is equal to the given title.
+ */
+ public function assertPageTitleSame(string $expectedTitle, string $message = ''): void
+ {
+ $this->assertSelectorTextSame('title', $expectedTitle, $message);
+ }
+
+ /**
+ * Asserts that the contents of the first element matching the given selector does equal the expected text.
+ */
+ public function assertSelectorTextSame(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new CrawlerSelectorTextSame($selector, $text)
+ ), $message);
+ }
+
+ /**
+ * Asserts that the `` element contains the given title.
+ */
+ public function assertPageTitleContains(string $expectedTitle, string $message = ''): void
+ {
+ $this->assertSelectorTextContains('title', $expectedTitle, $message);
+ }
+
+ /**
+ * Asserts that the first element matching the given selector does contain the expected text.
+ */
+ public function assertSelectorTextContains(string $selector, string $text, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists($selector),
+ new CrawlerSelectorTextContains($selector, $text)
+ ), $message);
+ }
+
+ /**
+ * Asserts that value of the form input with the given name does equal the expected value.
+ */
+ public function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists("input[name=\"$fieldName\"]"),
+ new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue)
+ ), $message);
+ }
+
+ /**
+ * Asserts that value of the form input with the given name does not equal the expected value.
+ */
+ public function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($this->getCrawler(), LogicalAnd::fromConstraints(
+ new CrawlerSelectorExists("input[name=\"$fieldName\"]"),
+ new LogicalNot(new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue))
+ ), $message);
+ }
+
+ /**
+ * Asserts that the checkbox with the given name is checked.
+ */
+ public function assertCheckboxChecked(string $fieldName, string $message = ''): void
+ {
+ $this->assertThat(
+ $this->getCrawler(),
+ new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked"),
+ $message
+ );
+ }
+
+ /**
+ * Asserts that the checkbox with the given name is not checked.
+ */
+ public function assertCheckboxNotChecked(string $fieldName, string $message = ''): void
+ {
+ $this->assertThat(
+ $this->getCrawler(),
+ new LogicalNot(new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked")),
+ $message
+ );
+ }
+
+ protected function getCrawler(): Crawler
+ {
+ return $this->client->getCrawler();
+ }
+}
diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php
index 930969c..a581158 100644
--- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php
@@ -4,6 +4,8 @@
namespace Codeception\Module\Symfony;
+use DateInterval;
+use DateTime;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollector;
use function array_key_exists;
use function in_array;
@@ -12,6 +14,29 @@
trait FormAssertionsTrait
{
+ /**
+ * Asserts that value of the field of the first form matching the given selector does equal the expected value.
+ */
+ public function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void
+ {
+ $node = $this->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 value of the field of the first form matching the given selector does equal the expected value.
+ */
+ public function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void
+ {
+ $node = $this->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.
*
@@ -33,6 +58,60 @@ public function dontSeeFormErrors(): void
);
}
+ /**
+ * Verifies that multiple fields on a form have errors.
+ *
+ * If you only specify the name of the fields, this method will
+ * verify that the field contains at least one error of any type:
+ *
+ * ```php
+ * seeFormErrorMessages(['telephone', 'address']);
+ * ```
+ *
+ * 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:
+ *
+ * ```php
+ * seeFormErrorMessages([
+ * 'address' => 'The address is too long'
+ * 'telephone' => 'too short', // the full error message is 'The telephone is too short'
+ * ]);
+ * ```
+ *
+ * If you don't want to specify the error message for some fields,
+ * you can pass `null` as value instead of the message string,
+ * or you can directly omit the value of that field. If that is the case,
+ * it will be validated that that field has at least one error of any type:
+ *
+ * ```php
+ * seeFormErrorMessages([
+ * 'telephone' => 'too short',
+ * 'address' => null,
+ * 'postal code',
+ * ]);
+ * ```
+ *
+ * @param string[] $expectedErrors
+ */
+ public function seeFormErrorMessages(array $expectedErrors): void
+ {
+ foreach ($expectedErrors as $field => $message) {
+ if (is_int($field)) {
+ $this->seeFormErrorMessage($message);
+ } else {
+ $this->seeFormErrorMessage($field, $message);
+ }
+ }
+ }
+
/**
* Verifies that a form field has an error.
* You can specify the expected error message as second parameter.
@@ -42,8 +121,6 @@ public function dontSeeFormErrors(): void
* $I->seeFormErrorMessage('username');
* $I->seeFormErrorMessage('username', 'Username is empty');
* ```
- *
- * @param string|null $message
*/
public function seeFormErrorMessage(string $field, ?string $message = null): void
{
@@ -93,60 +170,6 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi
);
}
- /**
- * Verifies that multiple fields on a form have errors.
- *
- * If you only specify the name of the fields, this method will
- * verify that the field contains at least one error of any type:
- *
- * ```php
- * seeFormErrorMessages(['telephone', 'address']);
- * ```
- *
- * 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:
- *
- * ```php
- * seeFormErrorMessages([
- * 'address' => 'The address is too long'
- * 'telephone' => 'too short', // the full error message is 'The telephone is too short'
- * ]);
- * ```
- *
- * If you don't want to specify the error message for some fields,
- * you can pass `null` as value instead of the message string,
- * or you can directly omit the value of that field. If that is the case,
- * it will be validated that that field has at least one error of any type:
- *
- * ```php
- * seeFormErrorMessages([
- * 'telephone' => 'too short',
- * 'address' => null,
- * 'postal code',
- * ]);
- * ```
- *
- * @param string[] $expectedErrors
- */
- public function seeFormErrorMessages(array $expectedErrors): void
- {
- foreach ($expectedErrors as $field => $message) {
- if (is_int($field)) {
- $this->seeFormErrorMessage($message);
- } else {
- $this->seeFormErrorMessage($field, $message);
- }
- }
- }
-
/**
* Verifies that there are one or more errors bound to the submitted form.
*
diff --git a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php
new file mode 100644
index 0000000..cc53274
--- /dev/null
+++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php
@@ -0,0 +1,117 @@
+getHttpClientDataCollector(__FUNCTION__);
+ $expectedRequestHasBeenFound = false;
+
+ if (!array_key_exists($httpClientId, $httpClientDataCollector->getClients())) {
+ $this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
+ }
+
+ foreach ($httpClientDataCollector->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 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.
+ */
+ public function assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client'): void
+ {
+ $httpClientDataCollector = $this->getHttpClientDataCollector(__FUNCTION__);
+ $unexpectedUrlHasBeenFound = false;
+
+ if (!array_key_exists($httpClientId, $httpClientDataCollector->getClients())) {
+ $this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
+ }
+
+ foreach ($httpClientDataCollector->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));
+ }
+
+ /**
+ * 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.
+ */
+ public function assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client'): void
+ {
+ $httpClientDataCollector = $this->getHttpClientDataCollector(__FUNCTION__);
+
+ $this->assertCount($count, $httpClientDataCollector->getClients()[$httpClientId]['traces']);
+ }
+
+ protected function getHttpClientDataCollector(string $function): HttpClientDataCollector
+ {
+ return $this->grabCollector('http_client', $function);
+ }
+}
diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php
index f8bb977..753a871 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;
@@ -11,6 +13,40 @@
trait MailerAssertionsTrait
{
+ /**
+ * Asserts that the expected number of emails was sent.
+ */
+ public function assertEmailCount(int $count, ?string $transport = null, string $message = ''): void
+ {
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport), $message);
+ }
+
+ /**
+ * Asserts that the expected number of emails was queued (e.g. using the Messenger component).
+ */
+ public function assertQueuedEmailCount(int $count, ?string $transport = null, string $message = ''): void
+ {
+ $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport, true), $message);
+ }
+
+ /**
+ * Asserts that the given mailer event is queued.
+ * Use `getMailerEvent(int $index = 0, ?string $transport = null)` to retrieve a mailer event by index.
+ */
+ public function assertEmailIsQueued(MessageEvent $event, string $message = ''): void
+ {
+ $this->assertThat($event, new MailerConstraint\EmailIsQueued(), $message);
+ }
+
+ /**
+ * Asserts that the given mailer event is not queued.
+ * Use `getMailerEvent(int $index = 0, ?string $transport = null)` to retrieve a mailer event by index.
+ */
+ public function assertEmailIsNotQueued(MessageEvent $event, string $message = ''): void
+ {
+ $this->assertThat($event, new LogicalNot(new MailerConstraint\EmailIsQueued()), $message);
+ }
+
/**
* Checks that no email was sent.
* The check is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
index d20ea30..5365fe1 100644
--- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php
@@ -6,10 +6,27 @@
use PHPUnit\Framework\Constraint\LogicalNot;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\RawMessage;
use Symfony\Component\Mime\Test\Constraint as MimeConstraint;
trait MimeAssertionsTrait
{
+ /**
+ * Asserts that the subject of the given email does contain the expected subject.
+ */
+ public function assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($email, new MimeConstraint\EmailSubjectContains($expectedValue), $message);
+ }
+
+ /**
+ * Asserts that the subject of the given email does not contain the expected subject.
+ */
+ public function assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailSubjectContains($expectedValue)), $message);
+ }
+
/**
* Verify that an email contains addresses with a [header](https://datatracker.ietf.org/doc/html/rfc4021)
* `$headerName` and its expected value `$expectedValue`.
diff --git a/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php b/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php
new file mode 100644
index 0000000..cfab668
--- /dev/null
+++ b/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php
@@ -0,0 +1,91 @@
+assertThat($this->getNotificationEvents(), new NotificationCount($count, $transportName), $message);
+ }
+
+ /**
+ * Asserts that the given number of notifications are queued (in total or for the given transport).
+ */
+ public function assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = ''): void
+ {
+ $this->assertThat($this->getNotificationEvents(), new NotificationCount($count, $transportName, true), $message);
+ }
+
+ /**
+ * Asserts that the given notification is queued.
+ */
+ public function assertNotificationIsQueued(MessageEvent $event, string $message = ''): void
+ {
+ $this->assertThat($event, new NotificationIsQueued(), $message);
+ }
+
+ /**
+ * Asserts that the given notification is not queued.
+ */
+ public function assertNotificationIsNotQueued(MessageEvent $event, string $message = ''): void
+ {
+ $this->assertThat($event, new LogicalNot(new NotificationIsQueued()), $message);
+ }
+
+ /**
+ * Asserts that the given text is included in the subject of the given notification.
+ */
+ public function assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = ''): void
+ {
+ $this->assertThat($notification, new NotificationSubjectContains($text), $message);
+ }
+
+ /**
+ * Asserts that the given text is not included in the subject of the given notification.
+ */
+ public function assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = ''): void
+ {
+ $this->assertThat($notification, new LogicalNot(new NotificationSubjectContains($text)), $message);
+ }
+
+ /**
+ * Asserts that the name of the transport for the given notification is the same as the given text.
+ */
+ public function assertNotificationTransportIsEqual(MessageInterface $notification, ?string $transportName = null, string $message = ''): void
+ {
+ $this->assertThat($notification, new NotificationTransportIsEqual($transportName), $message);
+ }
+
+ /**
+ * Asserts that the name of the transport for the given notification is not the same as the given text.
+ */
+ public function assertNotificationTransportIsNotEqual(MessageInterface $notification, ?string $transportName = null, string $message = ''): void
+ {
+ $this->assertThat($notification, new LogicalNot(new NotificationTransportIsEqual($transportName)), $message);
+ }
+
+ protected function getNotificationEvents(): NotificationEvents
+ {
+ $notificationLogger = $this->getService('notifier.notification_logger_listener');
+ if ($notificationLogger) {
+ return $notificationLogger->getEvents();
+ }
+
+ $this->fail('A client must have Notifier enabled to make notifications assertions. Did you forget to require symfony/notifier?');
+ }
+}