diff --git a/composer.json b/composer.json index 684c23c..79073cb 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "type": "library", "keywords": [ "codeception", + "functional testing", "symfony" ], "authors": [ @@ -37,6 +38,7 @@ "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", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index c508d5e..3f7917b 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -13,8 +13,10 @@ 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\ParameterAssertionsTrait; @@ -135,8 +137,10 @@ 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 ParameterAssertionsTrait; 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 `