From 6bf04ab3f03c752b9168b011f2c8d257b06a6a0b Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Mon, 13 Feb 2023 09:21:01 +0200 Subject: [PATCH 01/34] Install PHPUnit 10 for Symfony 6.1 and 6.2 --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab42b8c5..9cfbda23 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,6 +97,11 @@ jobs: run: composer validate working-directory: framework-tests + - name: Install PHPUnit 10 for Symfony 6.1 and 6.2 + if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*'" + run: composer require --dev --no-update "phpunit/phpunit=^10.0" + working-directory: framework-tests + - name: Install Symfony Sample run: | composer remove codeception/codeception codeception/module-asserts codeception/module-doctrine2 codeception/lib-innerbrowser codeception/module-symfony --dev --no-update From de2ac28868cabe493c8863888527d9d6030c7d1a Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Mon, 13 Feb 2023 09:52:04 +0200 Subject: [PATCH 02/34] Install PHPUnit 9 for Symfony 4.4, 5.4 and 6.0 --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9cfbda23..f0455aaf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,6 +79,10 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Install PHPUnit 9 for Symfony 4.4, 5.4 and 6.0 + if: "matrix.symfony == '4.4.*' || matrix.symfony == '5.4.*' || matrix.symfony == '6.0.*'" + run: composer require --dev --no-update "phpunit/phpunit=^9.0" + - name: Install dependencies run: | composer require symfony/finder=${{ matrix.symfony }} --no-update @@ -97,7 +101,7 @@ jobs: run: composer validate working-directory: framework-tests - - name: Install PHPUnit 10 for Symfony 6.1 and 6.2 + - name: Install PHPUnit 10 in framework-tests for Symfony 6.1 and 6.2 if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*'" run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests From beccd5e1366510df39e6055e60af9d03c63fc1d3 Mon Sep 17 00:00:00 2001 From: Mykhailo Sverdlykivskyi Date: Sat, 18 Feb 2023 17:56:51 +0200 Subject: [PATCH 03/34] Deprecate event triggered assertions (#169) --- .../Module/Symfony/EventsAssertionsTrait.php | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index f4b47b98..5a20be5b 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -56,8 +56,27 @@ public function dontSeeOrphanEvent(array|object|string $expected = null): void * ``` * * @param object|string|string[] $expected + * @deprecated Use `dontSeeEventListenerIsCalled()` instead. */ public function dontSeeEventTriggered(array|object|string $expected): void + { + trigger_error('dontSeeEventTriggered is deprecated, please use dontSeeEventListenerIsCalled instead', E_USER_DEPRECATED); + $this->dontSeeEventListenerIsCalled($expected); + } + + /** + * Verifies that one or more event listeners were not called during the test. + * + * ```php + * dontSeeEventListenerIsCalled('App\MyEventListener'); + * $I->dontSeeEventListenerIsCalled(new App\Events\MyEventListener()); + * $I->dontSeeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * ``` + * + * @param object|string|string[] $expected + */ + public function dontSeeEventListenerIsCalled(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -106,8 +125,27 @@ public function seeOrphanEvent(array|object|string $expected): void * ``` * * @param object|string|string[] $expected + * @deprecated Use `seeEventListenerIsCalled()` instead. */ public function seeEventTriggered(array|object|string $expected): void + { + trigger_error('seeEventTriggered is deprecated, please use seeEventListenerIsCalled instead', E_USER_DEPRECATED); + $this->seeEventListenerIsCalled($expected); + } + + /** + * Verifies that one or more event listeners were called during the test. + * + * ```php + * seeEventListenerIsCalled('App\MyEventListener'); + * $I->seeEventListenerIsCalled(new App\Events\MyEventListener()); + * $I->seeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * ``` + * + * @param object|string|string[] $expected + */ + public function seeEventListenerIsCalled(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -170,4 +208,4 @@ protected function grabEventCollector(string $function): EventDataCollector { return $this->grabCollector('events', $function); } -} \ No newline at end of file +} From cb1334090161aecc94bdb78c7ea3a52417cd728b Mon Sep 17 00:00:00 2001 From: Aaron Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:28:50 -0500 Subject: [PATCH 04/34] Various improvements (#171) * Traits improvements * Optimize imports --- .../Module/Symfony/BrowserAssertionsTrait.php | 1 - .../Module/Symfony/ConsoleAssertionsTrait.php | 8 ++- .../Symfony/DoctrineAssertionsTrait.php | 2 +- .../Module/Symfony/EventsAssertionsTrait.php | 6 -- .../Module/Symfony/FormAssertionsTrait.php | 4 +- .../Module/Symfony/MailerAssertionsTrait.php | 8 +-- .../Symfony/ParameterAssertionsTrait.php | 6 +- .../Module/Symfony/RouterAssertionsTrait.php | 10 +-- .../Symfony/ServicesAssertionsTrait.php | 12 +--- .../Module/Symfony/SessionAssertionsTrait.php | 71 ++++++++----------- .../Module/Symfony/TimeAssertionsTrait.php | 2 +- .../Module/Symfony/TwigAssertionsTrait.php | 8 +-- 12 files changed, 51 insertions(+), 87 deletions(-) diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index cabd34f2..67dc1ddb 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -23,7 +23,6 @@ trait BrowserAssertionsTrait * // Perform other requests * * ``` - * */ public function rebootClientKernel(): void { diff --git a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php index d54e2bfa..66edec9e 100644 --- a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php @@ -40,8 +40,12 @@ public function runSymfonyConsoleCommand(string $command, array $parameters = [] $this->assertSame( $expectedExitCode, $exitCode, - 'Command did not exit with code ' . $expectedExitCode - . ' but with ' . $exitCode . ': ' . $output + sprintf( + 'Command did not exit with code %d but with %d: %s', + $expectedExitCode, + $exitCode, + $output + ) ); return $output; diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php index 9aa4a054..6f9f0bc4 100644 --- a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php @@ -6,7 +6,6 @@ use Doctrine\ORM\EntityRepository; use function class_exists; -use function get_class; use function interface_exists; use function is_object; use function is_string; @@ -66,6 +65,7 @@ public function grabRepository(object|string $mixed): ?EntityRepository $getRepo = function () use ($mixed, $entityRepoClass, $isNotARepo): ?EntityRepository { if (!$repo = $this->grabService($mixed)) return null; + /** @var EntityRepository $repo */ if (!$repo instanceof $entityRepoClass) { $isNotARepo(); return null; diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 5a20be5b..04c3d081 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -6,10 +6,8 @@ use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; use Symfony\Component\VarDumper\Cloner\Data; -use function get_class; use function is_array; use function is_object; -use function strpos; trait EventsAssertionsTrait { @@ -34,7 +32,6 @@ public function dontSeeOrphanEvent(array|object|string $expected = null): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); - /** @var Data $data */ $data = $eventCollector->getOrphanedEvents(); $expected = is_array($expected) ? $expected : [$expected]; @@ -80,7 +77,6 @@ public function dontSeeEventListenerIsCalled(array|object|string $expected): voi { $eventCollector = $this->grabEventCollector(__FUNCTION__); - /** @var Data $data */ $data = $eventCollector->getCalledListeners(); $expected = is_array($expected) ? $expected : [$expected]; @@ -107,7 +103,6 @@ public function seeOrphanEvent(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); - /** @var Data $data */ $data = $eventCollector->getOrphanedEvents(); $expected = is_array($expected) ? $expected : [$expected]; @@ -149,7 +144,6 @@ public function seeEventListenerIsCalled(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); - /** @var Data $data */ $data = $eventCollector->getCalledListeners(); $expected = is_array($expected) ? $expected : [$expected]; diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 8261ea9f..31940e15 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -51,7 +51,7 @@ public function seeFormErrorMessage(string $field, string $message = null): void $formCollector = $this->grabFormCollector(__FUNCTION__); if (!$forms = $formCollector->getData()->getValue(true)['forms']) { - $this->fail('There are no forms on the current page.'); + $this->fail('No forms found on the current page.'); } $fields = []; @@ -73,7 +73,7 @@ public function seeFormErrorMessage(string $field, string $message = null): void } if (!in_array($field, $fields)) { - $this->fail("the field '{$field}' does not exist in the form."); + $this->fail("The field '{$field}' does not exist in the form."); } if (!array_key_exists($field, $errors)) { diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index cb82484a..4e61cb2d 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -56,13 +56,11 @@ public function seeEmailIsSent(int $expectedCount = 1): void */ public function grabLastSentEmail(): ?Email { + /** @var Email[] $emails */ $emails = $this->getMessageMailerEvents()->getMessages(); - /** @var Email|false $lastEmail */ - if ($lastEmail = end($emails)) { - return $lastEmail; - } + $lastEmail = end($emails); - return null; + return $lastEmail ?: null; } /** diff --git a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php index cb5bcad2..63231dd5 100644 --- a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php @@ -16,13 +16,13 @@ trait ParameterAssertionsTrait * $I->grabParameter('app.business_name'); * ``` * - * @param string $name + * @param string $parameterName * @return array|bool|float|int|string|null */ - public function grabParameter(string $name) + public function grabParameter(string $parameterName) { $parameterBag = $this->grabParameterBagService(); - return $parameterBag->get($name); + return $parameterBag->get($parameterName); } protected function grabParameterBagService(): ParameterBagInterface diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index c023782f..e0bdeab0 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -11,8 +11,6 @@ use function array_merge; use function explode; use function sprintf; -use function strlen; -use function substr_compare; trait RouterAssertionsTrait { @@ -32,7 +30,6 @@ trait RouterAssertionsTrait public function amOnAction(string $action, array $params = []): void { $router = $this->grabRouterService(); - $routes = $router->getRouteCollection()->getIterator(); foreach ($routes as $route) { @@ -66,7 +63,7 @@ public function amOnRoute(string $routeName, array $params = []): void { $router = $this->grabRouterService(); if ($router->getRouteCollection()->get($routeName) === null) { - $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); + $this->fail(sprintf('Route with name "%s" does not exist.', $routeName)); } $url = $router->generate($routeName, $params); @@ -95,7 +92,6 @@ public function invalidateCachedRouter(): void public function seeCurrentActionIs(string $action): void { $router = $this->grabRouterService(); - $routes = $router->getRouteCollection()->getIterator(); foreach ($routes as $route) { @@ -128,7 +124,7 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void { $router = $this->grabRouterService(); if ($router->getRouteCollection()->get($routeName) === null) { - $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); + $this->fail(sprintf('Route with name "%s" does not exist.', $routeName)); } $uri = explode('?', $this->grabFromCurrentUrl())[0]; @@ -160,7 +156,7 @@ public function seeInCurrentRoute(string $routeName): void { $router = $this->grabRouterService(); if ($router->getRouteCollection()->get($routeName) === null) { - $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); + $this->fail(sprintf('Route with name "%s" does not exist.', $routeName)); } $uri = explode('?', $this->grabFromCurrentUrl())[0]; diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php index b53a72b8..bd9140c0 100644 --- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -20,7 +20,6 @@ trait ServicesAssertionsTrait * ``` * * @part services - * @param string $serviceId */ public function grabService(string $serviceId): object { @@ -69,15 +68,10 @@ public function persistPermanentService(string $serviceName): void */ public function unpersistService(string $serviceName): void { - if (isset($this->persistentServices[$serviceName])) { - unset($this->persistentServices[$serviceName]); - } - - if (isset($this->permanentServices[$serviceName])) { - unset($this->permanentServices[$serviceName]); - } + unset($this->persistentServices[$serviceName]); + unset($this->permanentServices[$serviceName]); - if ($this->client instanceof SymfonyConnector && isset($this->client->persistentServices[$serviceName])) { + if ($this->client instanceof SymfonyConnector) { unset($this->client->persistentServices[$serviceName]); } } diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index f6911303..a8b69afd 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -29,37 +29,17 @@ trait SessionAssertionsTrait * ]); * $I->amLoggedInAs($user); * ``` - * - * @param UserInterface $user - * @param string $firewallName - * @param null $firewallContext */ - public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', $firewallContext = null): void + public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', string $firewallContext = null): void { $session = $this->getCurrentSession(); + $roles = $user->getRoles(); - if ($this->getSymfonyMajorVersion() < 6) { - if ($this->config['guard']) { - $token = new PostAuthenticationGuardToken($user, $firewallName, $user->getRoles()); - } else { - $token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles()); - } - } else { - if ($this->config['authenticator']) { - $token = new PostAuthenticationToken($user, $firewallName, $user->getRoles()); - } else { - $token = new UsernamePasswordToken($user, $firewallName, $user->getRoles()); - } - } - + $token = $this->createAuthenticationToken($user, $firewallName, $roles); $this->getTokenStorage()->setToken($token); - if ($firewallContext) { - $session->set('_security_' . $firewallContext, serialize($token)); - } else { - $session->set('_security_' . $firewallName, serialize($token)); - } - + $sessionKey = $firewallContext ? "_security_{$firewallContext}" : "_security_{$firewallName}"; + $session->set($sessionKey, serialize($token)); $session->save(); $cookie = new Cookie($session->getName(), $session->getId()); @@ -74,16 +54,13 @@ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', * $I->dontSeeInSession('attribute'); * $I->dontSeeInSession('attribute', 'value'); * ``` - * */ public function dontSeeInSession(string $attribute, mixed $value = null): void { $session = $this->getCurrentSession(); - if ($attributeExists = $session->has($attribute)) { - $this->fail("Session attribute with name '{$attribute}' does exist"); - } - $this->assertFalse($attributeExists); + $attributeExists = $session->has($attribute); + $this->assertFalse($attributeExists, "Session attribute '{$attribute}' exists."); if (null !== $value) { $this->assertNotSame($value, $session->get($attribute)); @@ -98,8 +75,7 @@ public function dontSeeInSession(string $attribute, mixed $value = null): void */ public function goToLogoutPath(): void { - $logoutUrlGenerator = $this->getLogoutUrlGenerator(); - $logoutPath = $logoutUrlGenerator->getLogoutPath(); + $logoutPath = $this->getLogoutUrlGenerator()->getLogoutPath(); $this->amOnPage($logoutPath); } @@ -132,17 +108,14 @@ public function logoutProgrammatically(): void } $session = $this->getCurrentSession(); - $sessionName = $session->getName(); $session->invalidate(); $cookieJar = $this->client->getCookieJar(); + $cookiesToExpire = ['MOCKSESSID', 'REMEMBERME', $sessionName]; foreach ($cookieJar->all() as $cookie) { $cookieName = $cookie->getName(); - if ($cookieName === 'MOCKSESSID' || - $cookieName === 'REMEMBERME' || - $cookieName === $sessionName - ) { + if (in_array($cookieName, $cookiesToExpire, true)) { $cookieJar->expire($cookieName); } } @@ -163,10 +136,8 @@ public function seeInSession(string $attribute, mixed $value = null): void { $session = $this->getCurrentSession(); - if (!$attributeExists = $session->has($attribute)) { - $this->fail("No session attribute with name '{$attribute}'"); - } - $this->assertTrue($attributeExists); + $attributeExists = $session->has($attribute); + $this->assertTrue($attributeExists, "No session attribute with name '{$attribute}'"); if (null !== $value) { $this->assertSame($value, $session->get($attribute)); @@ -181,8 +152,6 @@ public function seeInSession(string $attribute, mixed $value = null): void * $I->seeSessionHasValues(['key1', 'key2']); * $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); * ``` - * - * @param array $bindings */ public function seeSessionHasValues(array $bindings): void { @@ -227,4 +196,20 @@ protected function getSymfonyMajorVersion(): int { return $this->kernel::MAJOR_VERSION; } + + /** + * @return UsernamePasswordToken|PostAuthenticationGuardToken|PostAuthenticationToken + */ + protected function createAuthenticationToken(UserInterface $user, string $firewallName, array $roles) + { + 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); + } } diff --git a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php index d48222eb..136bef25 100644 --- a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php @@ -36,7 +36,7 @@ public function seeRequestTimeIsLessThan(int|float $expectedMilliseconds): void $expectedMilliseconds, $actualMilliseconds, sprintf( - 'The request was expected to last less than %d ms, but it actually lasted %d ms.', + 'The request duration was expected to be less than %d ms, but it was actually %d ms.', $expectedMilliseconds, $actualMilliseconds ) diff --git a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php index 624b822e..52b02d0d 100644 --- a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php @@ -16,8 +16,6 @@ trait TwigAssertionsTrait * dontSeeRenderedTemplate('home.html.twig'); * ``` - * - * @param string $template */ public function dontSeeRenderedTemplate(string $template): void { @@ -39,15 +37,13 @@ public function dontSeeRenderedTemplate(string $template): void * seeCurrentTemplateIs('home.html.twig'); * ``` - * - * @param string $expectedTemplate */ public function seeCurrentTemplateIs(string $expectedTemplate): void { $twigCollector = $this->grabTwigCollector(__FUNCTION__); $templates = (array)$twigCollector->getTemplates(); - $actualTemplate = (string)array_key_first($templates); + $actualTemplate = !empty($templates) ? (string) array_key_first($templates) : 'N/A'; $this->assertSame( $expectedTemplate, @@ -65,8 +61,6 @@ public function seeCurrentTemplateIs(string $expectedTemplate): void * $I->seeRenderedTemplate('home.html.twig'); * $I->seeRenderedTemplate('layout.html.twig'); * ``` - * - * @param string $template */ public function seeRenderedTemplate(string $template): void { From 19c86fc1041fc29b151ec9a783426089731dbae7 Mon Sep 17 00:00:00 2001 From: Mykhailo Sverdlykivskyi Date: Mon, 11 Dec 2023 21:10:21 +0000 Subject: [PATCH 05/34] Update event assertions (#168) * Add event dispatch assertions, event listener call assertions (deprecate existing ones) * fix wrong method call * Remove duplicated methods, fix PHPCS warnings * Restore lost deprecation warnings, reorder methods to keep in line with upstream * Update seeEventListenerCalled/dontSeeEventListenerCalled to support check with event --------- Co-authored-by: TavoNiievez --- .../Module/Symfony/EventsAssertionsTrait.php | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 04c3d081..81381a05 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -6,6 +6,7 @@ use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; use Symfony\Component\VarDumper\Cloner\Data; + use function is_array; use function is_object; @@ -53,11 +54,14 @@ public function dontSeeOrphanEvent(array|object|string $expected = null): void * ``` * * @param object|string|string[] $expected - * @deprecated Use `dontSeeEventListenerIsCalled()` instead. + * @deprecated Use `dontSeeEventListenerIsCalled` instead. */ public function dontSeeEventTriggered(array|object|string $expected): void { - trigger_error('dontSeeEventTriggered is deprecated, please use dontSeeEventListenerIsCalled instead', E_USER_DEPRECATED); + trigger_error( + 'dontSeeEventTriggered is deprecated, please use dontSeeEventListenerIsCalled instead', + E_USER_DEPRECATED + ); $this->dontSeeEventListenerIsCalled($expected); } @@ -69,18 +73,28 @@ public function dontSeeEventTriggered(array|object|string $expected): void * $I->dontSeeEventListenerIsCalled('App\MyEventListener'); * $I->dontSeeEventListenerIsCalled(new App\Events\MyEventListener()); * $I->dontSeeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * $I->dontSeeEventListenerIsCalled('App\MyEventListener', 'my.event); + * $I->dontSeeEventListenerIsCalled(new App\Events\MyEventListener(), new MyEvent()); + * $I->dontSeeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); * ``` * * @param object|string|string[] $expected */ - public function dontSeeEventListenerIsCalled(array|object|string $expected): void - { + public function dontSeeEventListenerIsCalled( + array|object|string $expected, + array|object|string $withEvents = [] + ): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); $data = $eventCollector->getCalledListeners(); $expected = is_array($expected) ? $expected : [$expected]; + $withEvents = is_array($withEvents) ? $withEvents : [$withEvents]; + + if (!empty($withEvents) && count($expected) > 1) { + $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.'); + } - $this->assertEventNotTriggered($data, $expected); + $this->assertListenerCalled($data, $expected, $withEvents, true); } /** @@ -120,11 +134,14 @@ public function seeOrphanEvent(array|object|string $expected): void * ``` * * @param object|string|string[] $expected - * @deprecated Use `seeEventListenerIsCalled()` instead. + * @deprecated Use `seeEventListenerIsCalled` instead. */ public function seeEventTriggered(array|object|string $expected): void { - trigger_error('seeEventTriggered is deprecated, please use seeEventListenerIsCalled instead', E_USER_DEPRECATED); + trigger_error( + 'seeEventTriggered is deprecated, please use seeEventListenerIsCalled instead', + E_USER_DEPRECATED + ); $this->seeEventListenerIsCalled($expected); } @@ -136,18 +153,28 @@ public function seeEventTriggered(array|object|string $expected): void * $I->seeEventListenerIsCalled('App\MyEventListener'); * $I->seeEventListenerIsCalled(new App\Events\MyEventListener()); * $I->seeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * $I->seeEventListenerIsCalled('App\MyEventListener', 'my.event); + * $I->seeEventListenerIsCalled(new App\Events\MyEventListener(), new MyEvent()); + * $I->seeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); * ``` * * @param object|string|string[] $expected */ - public function seeEventListenerIsCalled(array|object|string $expected): void - { + public function seeEventListenerIsCalled( + array|object|string $expected, + array|object|string $withEvents = [] + ): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); $data = $eventCollector->getCalledListeners(); $expected = is_array($expected) ? $expected : [$expected]; + $withEvents = is_array($withEvents) ? $withEvents : [$withEvents]; - $this->assertEventTriggered($data, $expected); + if (!empty($withEvents) && count($expected) > 1) { + $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.'); + } + + $this->assertListenerCalled($data, $expected, $withEvents); } protected function assertEventNotTriggered(Data $data, array $expected): void @@ -180,6 +207,39 @@ protected function assertEventTriggered(Data $data, array $expected): void } } + protected function assertListenerCalled( + Data $data, + array $expectedListeners, + array $withEvents, + bool $invertAssertion = false + ): void { + $assertTrue = !$invertAssertion; + + if ($assertTrue && $data->count() === 0) { + $this->fail('No event listener was called'); + } + + $actual = $data->getValue(true); + $expectedEvents = empty($withEvents) ? [null] : $withEvents; + + foreach ($expectedListeners as $expectedListener) { + $expectedListener = is_object($expectedListener) ? $expectedListener::class : $expectedListener; + + foreach ($expectedEvents as $expectedEvent) { + $message = "The '{$expectedListener}' listener was called" + . ($expectedEvent ? " for the '{$expectedEvent}' event" : ''); + + $condition = $this->listenerWasCalled($actual, $expectedListener, $expectedEvent); + + if ($assertTrue) { + $this->assertTrue($condition, $message); + } else { + $this->assertFalse($condition, $message); + } + } + } + } + protected function eventWasTriggered(array $actual, string $expectedEvent): bool { $triggered = false; @@ -195,9 +255,28 @@ protected function eventWasTriggered(array $actual, string $expectedEvent): bool } } } + return $triggered; } + protected function listenerWasCalled(array $actual, string $expectedListener, string|null $expectedEvent): bool + { + $called = false; + + foreach ($actual as $actualEvent) { + // Called Listeners + if (is_array($actualEvent) && str_starts_with($actualEvent['pretty'], $expectedListener)) { + if ($expectedEvent === null) { + $called = true; + } elseif ($actualEvent['event'] === $expectedEvent) { + $called = true; + } + } + } + + return $called; + } + protected function grabEventCollector(string $function): EventDataCollector { return $this->grabCollector('events', $function); From 6c8b03cc8d07c0bfeb37a73ddce960ce237406bf Mon Sep 17 00:00:00 2001 From: Mykhailo Sverdlykivskyi Date: Wed, 20 Dec 2023 15:30:55 +0200 Subject: [PATCH 06/34] Add seeEvent/dontSeeEvent event assertions (#173) * Add seeEvent/dontSeeEvent --- .../Module/Symfony/EventsAssertionsTrait.php | 117 ++++++++++++++---- 1 file changed, 91 insertions(+), 26 deletions(-) diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 81381a05..f6d56cc3 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -39,7 +39,40 @@ public function dontSeeOrphanEvent(array|object|string $expected = null): void if ($expected === null) { $this->assertSame(0, $data->count()); } else { - $this->assertEventNotTriggered($data, $expected); + $this->assertEventTriggered($data, $expected, true); + } + } + + /** + * Verifies that there were no events during the test. + * Both regular and orphan events are checked. + * + * ```php + * dontSeeEvent(); + * $I->dontSeeEvent('App\MyEvent'); + * $I->dontSeeEvent(new App\Events\MyEvent()); + * $I->dontSeeEvent(['App\MyEvent', 'App\MyOtherEvent']); + * ``` + * + * @param array|object|string|null $expected + */ + public function dontSeeEvent(array|object|string $expected = null): void + { + $eventCollector = $this->grabEventCollector(__FUNCTION__); + + $data = [ + $eventCollector->getOrphanedEvents(), + $eventCollector->getCalledListeners(), + ]; + $expected = is_array($expected) ? $expected : [$expected]; + + if ($expected === null) { + foreach ($data as $dataItem) { + $this->assertSame(0, $dataItem->count()); + } + } else { + $this->assertEventTriggered($data, $expected, true); } } @@ -123,6 +156,35 @@ public function seeOrphanEvent(array|object|string $expected): void $this->assertEventTriggered($data, $expected); } + /** + * Verifies that one or more events were dispatched during the test. + * Both regular and orphan events are checked. + * + * If you need to verify that expected event is not orphan, + * add `dontSeeOrphanEvent` call. + * + * ```php + * seeEvent('App\MyEvent'); + * $I->seeEvent(new App\Events\MyEvent()); + * $I->seeEvent(['App\MyEvent', 'App\MyOtherEvent']); + * ``` + * + * @param array|object|string $expected + */ + public function seeEvent(array|object|string $expected): void + { + $eventCollector = $this->grabEventCollector(__FUNCTION__); + + $data = [ + $eventCollector->getOrphanedEvents(), + $eventCollector->getCalledListeners(), + ]; + $expected = is_array($expected) ? $expected : [$expected]; + + $this->assertEventTriggered($data, $expected); + } + /** * Verifies that one or more event listeners were called during the test. * @@ -177,33 +239,38 @@ public function seeEventListenerIsCalled( $this->assertListenerCalled($data, $expected, $withEvents); } - protected function assertEventNotTriggered(Data $data, array $expected): void - { - $actual = $data->getValue(true); - - foreach ($expected as $expectedEvent) { - $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent; - $this->assertFalse( - $this->eventWasTriggered($actual, (string)$expectedEvent), - "The '{$expectedEvent}' event triggered" - ); - } - } + protected function assertEventTriggered( + array|Data $data, + array $expected, + bool $invertAssertion = false + ): void { + $assertTrue = !$invertAssertion; + $data = is_array($data) ? $data : [$data]; + $totalEvents = array_sum(array_map('count', $data)); - protected function assertEventTriggered(Data $data, array $expected): void - { - if ($data->count() === 0) { + if ($assertTrue && $totalEvents === 0) { $this->fail('No event was triggered'); } - $actual = $data->getValue(true); + $actualEventsCollection = array_map(static fn (Data $data) => $data->getValue(true), $data); foreach ($expected as $expectedEvent) { $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent; - $this->assertTrue( - $this->eventWasTriggered($actual, (string)$expectedEvent), - "The '{$expectedEvent}' event did not trigger" - ); + $message = $assertTrue + ? "The '{$expectedEvent}' event did not trigger" + : "The '{$expectedEvent}' event triggered"; + + $eventTriggered = false; + + foreach ($actualEventsCollection as $actualEvents) { + $eventTriggered = $eventTriggered || $this->eventWasTriggered($actualEvents, (string)$expectedEvent); + } + + if ($assertTrue) { + $this->assertTrue($eventTriggered, $message); + } else { + $this->assertFalse($eventTriggered, $message); + } } } @@ -246,13 +313,11 @@ protected function eventWasTriggered(array $actual, string $expectedEvent): bool foreach ($actual as $actualEvent) { if (is_array($actualEvent)) { // Called Listeners - if (str_starts_with($actualEvent['pretty'], $expectedEvent)) { - $triggered = true; - } - } else { // Orphan Events - if ($actualEvent === $expectedEvent) { + if ($actualEvent['event'] === $expectedEvent) { $triggered = true; } + } elseif ($actualEvent === $expectedEvent) { // Orphan Events + $triggered = true; } } From fb80fccef4f1de982ba7d75aaa1143d43938da13 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Fri, 22 Dec 2023 23:02:00 -0500 Subject: [PATCH 07/34] Support Symfony 6.3 --- .github/workflows/main.yml | 14 ++++++++++++-- composer.json | 18 +++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f0455aaf..885dd616 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,12 +9,14 @@ jobs: strategy: matrix: php: [8.0, 8.1, 8.2] - symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*", "6.2.*"] + symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*", "6.2.*", "6.3.*"] exclude: - php: 8.0 symfony: "6.1.*" - php: 8.0 symfony: "6.2.*" + - php: 8.0 + symfony: "6.3.*" steps: - name: Checkout code @@ -68,6 +70,14 @@ jobs: path: framework-tests ref: "6.2" + - name: Checkout Symfony 6.3 Sample + if: "matrix.symfony == '6.3.*'" + uses: actions/checkout@v2 + with: + repository: Codeception/symfony-module-tests + path: framework-tests + ref: "6.3" + - name: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" @@ -102,7 +112,7 @@ jobs: working-directory: framework-tests - name: Install PHPUnit 10 in framework-tests for Symfony 6.1 and 6.2 - if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*'" + if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*' || matrix.symfony == '6.3.*'" run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests diff --git a/composer.json b/composer.json index 1e03edcd..24d5822d 100644 --- a/composer.json +++ b/composer.json @@ -20,20 +20,36 @@ "require": { "php": "^8.0", "ext-json": "*", - "codeception/codeception": "^5.0.0-RC3", + "codeception/codeception": "^5.0.8", "codeception/lib-innerbrowser": "^3.1.1 | ^4.0" }, "require-dev": { "codeception/module-asserts": "^3.0", "codeception/module-doctrine2": "^3.0", "doctrine/orm": "^2.10", + "symfony/browser-kit": "^4.4 | ^5.0 | ^6.0", + "symfony/cache": "^4.4 | ^5.0 | ^6.0", + "symfony/config": "^4.4 | ^5.0 | ^6.0", + "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0", + "symfony/dom-crawler": "^4.4 | ^5.0 | ^6.0", + "symfony/error-handler": "^4.4 | ^5.0 | ^6.0", + "symfony/filesystem": "^4.4 | ^5.0 | ^6.0", "symfony/form": "^4.4 | ^5.0 | ^6.0", "symfony/framework-bundle": "^4.4 | ^5.0 | ^6.0", + "symfony/http-foundation": "^4.4 | ^5.0 | ^6.0", "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0", "symfony/mailer": "^4.4 | ^5.0 | ^6.0", + "symfony/mime": "^4.4 | ^5.0 | ^6.0", + "symfony/options-resolver": "^4.4 | ^5.0 | ^6.0", + "symfony/property-access": "^4.4 | ^5.0 | ^6.0", + "symfony/property-info": "^4.4 | ^5.0 | ^6.0", "symfony/routing": "^4.4 | ^5.0 | ^6.0", "symfony/security-bundle": "^4.4 | ^5.0 | ^6.0", + "symfony/security-core": "^4.4 | ^5.0 | ^6.0", + "symfony/security-csrf": "^4.4 | ^5.0 | ^6.0", + "symfony/security-http": "^4.4 | ^5.0 | ^6.0", "symfony/twig-bundle": "^4.4 | ^5.0 | ^6.0", + "symfony/var-exporter": "^4.4 | ^5.0 | ^6.0", "vlucas/phpdotenv": "^4.2 | ^5.4" }, "suggest": { From 5798e3e6328d20e9c41a5d1680f245e9881a4a43 Mon Sep 17 00:00:00 2001 From: Tavo Nieves J Date: Fri, 22 Dec 2023 23:02:09 -0500 Subject: [PATCH 08/34] EventsAssertionsTrait refactor --- .../Module/Symfony/EventsAssertionsTrait.php | 312 +++++++----------- 1 file changed, 115 insertions(+), 197 deletions(-) diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index f6d56cc3..0ef1ee4c 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -5,44 +5,11 @@ namespace Codeception\Module\Symfony; use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; -use Symfony\Component\VarDumper\Cloner\Data; - use function is_array; use function is_object; trait EventsAssertionsTrait { - /** - * Verifies that there were no orphan events during the test. - * - * An orphan event is an event that was triggered by manually executing the - * [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method - * of the EventDispatcher but was not handled by any listener after it was dispatched. - * - * ```php - * dontSeeOrphanEvent(); - * $I->dontSeeOrphanEvent('App\MyEvent'); - * $I->dontSeeOrphanEvent(new App\Events\MyEvent()); - * $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); - * ``` - * - * @param object|string|string[] $expected - */ - public function dontSeeOrphanEvent(array|object|string $expected = null): void - { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = $eventCollector->getOrphanedEvents(); - $expected = is_array($expected) ? $expected : [$expected]; - - if ($expected === null) { - $this->assertSame(0, $data->count()); - } else { - $this->assertEventTriggered($data, $expected, true); - } - } - /** * Verifies that there were no events during the test. * Both regular and orphan events are checked. @@ -51,29 +18,35 @@ public function dontSeeOrphanEvent(array|object|string $expected = null): void * dontSeeEvent(); * $I->dontSeeEvent('App\MyEvent'); - * $I->dontSeeEvent(new App\Events\MyEvent()); * $I->dontSeeEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param array|object|string|null $expected + * @param string|string[]|null $expected */ - public function dontSeeEvent(array|object|string $expected = null): void + public function dontSeeEvent(array|string $expected = null): void { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = [ - $eventCollector->getOrphanedEvents(), - $eventCollector->getCalledListeners(), - ]; - $expected = is_array($expected) ? $expected : [$expected]; + $actualEvents = array_merge(array_column($this->getCalledListeners(), 'event')); + $actual = [$this->getOrphanedEvents(), $actualEvents]; + $this->assertEventTriggered(false, $expected, $actual); + } - if ($expected === null) { - foreach ($data as $dataItem) { - $this->assertSame(0, $dataItem->count()); - } - } else { - $this->assertEventTriggered($data, $expected, true); - } + /** + * Verifies that one or more event listeners were not called during the test. + * + * ```php + * dontSeeEventListenerIsCalled('App\MyEventListener'); + * $I->dontSeeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * $I->dontSeeEventListenerIsCalled('App\MyEventListener', 'my.event); + * $I->dontSeeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); + * ``` + * + * @param class-string|class-string[] $expected + * @param string|string[] $events + */ + public function dontSeeEventListenerIsCalled(array|object|string $expected, array|string $events = []): void + { + $this->assertListenerCalled(false, $expected, $events); } /** @@ -99,39 +72,7 @@ public function dontSeeEventTriggered(array|object|string $expected): void } /** - * Verifies that one or more event listeners were not called during the test. - * - * ```php - * dontSeeEventListenerIsCalled('App\MyEventListener'); - * $I->dontSeeEventListenerIsCalled(new App\Events\MyEventListener()); - * $I->dontSeeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); - * $I->dontSeeEventListenerIsCalled('App\MyEventListener', 'my.event); - * $I->dontSeeEventListenerIsCalled(new App\Events\MyEventListener(), new MyEvent()); - * $I->dontSeeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); - * ``` - * - * @param object|string|string[] $expected - */ - public function dontSeeEventListenerIsCalled( - array|object|string $expected, - array|object|string $withEvents = [] - ): void { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = $eventCollector->getCalledListeners(); - $expected = is_array($expected) ? $expected : [$expected]; - $withEvents = is_array($withEvents) ? $withEvents : [$withEvents]; - - if (!empty($withEvents) && count($expected) > 1) { - $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.'); - } - - $this->assertListenerCalled($data, $expected, $withEvents, true); - } - - /** - * Verifies that one or more orphan events were dispatched during the test. + * Verifies that there were no orphan events during the test. * * An orphan event is an event that was triggered by manually executing the * [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method @@ -139,21 +80,17 @@ public function dontSeeEventListenerIsCalled( * * ```php * seeOrphanEvent('App\MyEvent'); - * $I->seeOrphanEvent(new App\Events\MyEvent()); - * $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); + * $I->dontSeeOrphanEvent(); + * $I->dontSeeOrphanEvent('App\MyEvent'); + * $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param object|string|string[] $expected + * @param string|string[] $expected */ - public function seeOrphanEvent(array|object|string $expected): void + public function dontSeeOrphanEvent(array|string $expected = null): void { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = $eventCollector->getOrphanedEvents(); - $expected = is_array($expected) ? $expected : [$expected]; - - $this->assertEventTriggered($data, $expected); + $actual = [$this->getOrphanedEvents()]; + $this->assertEventTriggered(false, $expected, $actual); } /** @@ -166,23 +103,35 @@ public function seeOrphanEvent(array|object|string $expected): void * ```php * seeEvent('App\MyEvent'); - * $I->seeEvent(new App\Events\MyEvent()); * $I->seeEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param array|object|string $expected + * @param string|string[] $expected */ - public function seeEvent(array|object|string $expected): void + public function seeEvent(array|string $expected): void { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = [ - $eventCollector->getOrphanedEvents(), - $eventCollector->getCalledListeners(), - ]; - $expected = is_array($expected) ? $expected : [$expected]; + $actualEvents = array_merge(array_column($this->getCalledListeners(), 'event')); + $actual = [$this->getOrphanedEvents(), $actualEvents]; + $this->assertEventTriggered(true, $expected, $actual); + } - $this->assertEventTriggered($data, $expected); + /** + * Verifies that one or more event listeners were called during the test. + * + * ```php + * seeEventListenerIsCalled('App\MyEventListener'); + * $I->seeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); + * $I->seeEventListenerIsCalled('App\MyEventListener', 'my.event); + * $I->seeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); + * ``` + * + * @param class-string|class-string[] $expected + * @param string|string[] $events + */ + public function seeEventListenerIsCalled(array|object|string $expected, array|string $events = []): void + { + $this->assertListenerCalled(true, $expected, $events); } /** @@ -208,138 +157,107 @@ public function seeEventTriggered(array|object|string $expected): void } /** - * Verifies that one or more event listeners were called during the test. + * Verifies that one or more orphan events were dispatched during the test. + * + * An orphan event is an event that was triggered by manually executing the + * [`dispatch()`](https://symfony.com/doc/current/components/event_dispatcher.html#dispatch-the-event) method + * of the EventDispatcher but was not handled by any listener after it was dispatched. * * ```php * seeEventListenerIsCalled('App\MyEventListener'); - * $I->seeEventListenerIsCalled(new App\Events\MyEventListener()); - * $I->seeEventListenerIsCalled(['App\MyEventListener', 'App\MyOtherEventListener']); - * $I->seeEventListenerIsCalled('App\MyEventListener', 'my.event); - * $I->seeEventListenerIsCalled(new App\Events\MyEventListener(), new MyEvent()); - * $I->seeEventListenerIsCalled('App\MyEventListener', ['my.event', 'my.other.event']); + * $I->seeOrphanEvent('App\MyEvent'); + * $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param object|string|string[] $expected + * @param string|string[] $expected */ - public function seeEventListenerIsCalled( - array|object|string $expected, - array|object|string $withEvents = [] - ): void { - $eventCollector = $this->grabEventCollector(__FUNCTION__); - - $data = $eventCollector->getCalledListeners(); - $expected = is_array($expected) ? $expected : [$expected]; - $withEvents = is_array($withEvents) ? $withEvents : [$withEvents]; + public function seeOrphanEvent(array|string $expected): void + { + $actual = [$this->getOrphanedEvents()]; + $this->assertEventTriggered(true, $expected, $actual); + } - if (!empty($withEvents) && count($expected) > 1) { - $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.'); - } + protected function getCalledListeners(): array + { + $eventCollector = $this->grabEventCollector(__FUNCTION__); + $calledListeners = $eventCollector->getCalledListeners($this->getDefaultDispatcher()); + return [...$calledListeners->getValue(true)]; + } - $this->assertListenerCalled($data, $expected, $withEvents); + protected function getOrphanedEvents(): array + { + $eventCollector = $this->grabEventCollector(__FUNCTION__); + $orphanedEvents = $eventCollector->getOrphanedEvents($this->getDefaultDispatcher()); + return [...$orphanedEvents->getValue(true)]; } - protected function assertEventTriggered( - array|Data $data, - array $expected, - bool $invertAssertion = false - ): void { - $assertTrue = !$invertAssertion; - $data = is_array($data) ? $data : [$data]; - $totalEvents = array_sum(array_map('count', $data)); + protected function assertEventTriggered(bool $assertTrue, array|object|string|null $expected, array $actual): void + { + $actualEvents = array_merge(...$actual); - if ($assertTrue && $totalEvents === 0) { - $this->fail('No event was triggered'); + if ($assertTrue) $this->assertNotEmpty($actualEvents, 'No event was triggered'); + if ($expected === null) { + $this->assertEmpty($actualEvents); + return; } - $actualEventsCollection = array_map(static fn (Data $data) => $data->getValue(true), $data); - - foreach ($expected as $expectedEvent) { + $expected = is_object($expected) ? $expected::class : $expected; + foreach ((array)$expected as $expectedEvent) { $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent; + $eventTriggered = in_array($expectedEvent, $actualEvents); + $message = $assertTrue ? "The '{$expectedEvent}' event did not trigger" : "The '{$expectedEvent}' event triggered"; - - $eventTriggered = false; - - foreach ($actualEventsCollection as $actualEvents) { - $eventTriggered = $eventTriggered || $this->eventWasTriggered($actualEvents, (string)$expectedEvent); - } - - if ($assertTrue) { - $this->assertTrue($eventTriggered, $message); - } else { - $this->assertFalse($eventTriggered, $message); - } + $this->assertSame($assertTrue, $eventTriggered, $message); } } - protected function assertListenerCalled( - Data $data, - array $expectedListeners, - array $withEvents, - bool $invertAssertion = false - ): void { - $assertTrue = !$invertAssertion; + protected function assertListenerCalled(bool $assertTrue, array|object|string $expectedListeners, array|object|string $expectedEvents): void + { + $expectedListeners = is_array($expectedListeners) ? $expectedListeners : [$expectedListeners]; + $expectedEvents = is_array($expectedEvents) ? $expectedEvents : [$expectedEvents]; - if ($assertTrue && $data->count() === 0) { - $this->fail('No event listener was called'); + if (empty($expectedEvents)) { + $expectedEvents = [null]; + } elseif (count($expectedListeners) > 1) { + $this->fail('You cannot check for events when using multiple listeners. Make multiple assertions instead.'); } - $actual = $data->getValue(true); - $expectedEvents = empty($withEvents) ? [null] : $withEvents; + $actualEvents = $this->getCalledListeners(); + if ($assertTrue && empty($actualEvents)) { + $this->fail('No event listener was called'); + } foreach ($expectedListeners as $expectedListener) { $expectedListener = is_object($expectedListener) ? $expectedListener::class : $expectedListener; foreach ($expectedEvents as $expectedEvent) { + $listenerCalled = $this->listenerWasCalled($expectedListener, $expectedEvent, $actualEvents); $message = "The '{$expectedListener}' listener was called" . ($expectedEvent ? " for the '{$expectedEvent}' event" : ''); - - $condition = $this->listenerWasCalled($actual, $expectedListener, $expectedEvent); - - if ($assertTrue) { - $this->assertTrue($condition, $message); - } else { - $this->assertFalse($condition, $message); - } + $this->assertSame($assertTrue, $listenerCalled, $message); } } } - protected function eventWasTriggered(array $actual, string $expectedEvent): bool + private function listenerWasCalled(string $expectedListener, ?string $expectedEvent, array $actualEvents): bool { - $triggered = false; - - foreach ($actual as $actualEvent) { - if (is_array($actualEvent)) { // Called Listeners - if ($actualEvent['event'] === $expectedEvent) { - $triggered = true; - } - } elseif ($actualEvent === $expectedEvent) { // Orphan Events - $triggered = true; + foreach ($actualEvents as $actualEvent) { + if ( + isset($actualEvent['pretty'], $actualEvent['event']) + && str_starts_with($actualEvent['pretty'], $expectedListener) + && ($expectedEvent === null || $actualEvent['event'] === $expectedEvent) + ) { + return true; } } - - return $triggered; + return false; } - protected function listenerWasCalled(array $actual, string $expectedListener, string|null $expectedEvent): bool + protected function getDefaultDispatcher(): string { - $called = false; - - foreach ($actual as $actualEvent) { - // Called Listeners - if (is_array($actualEvent) && str_starts_with($actualEvent['pretty'], $expectedListener)) { - if ($expectedEvent === null) { - $called = true; - } elseif ($actualEvent['event'] === $expectedEvent) { - $called = true; - } - } - } - - return $called; + return 'event_dispatcher'; } protected function grabEventCollector(string $function): EventDataCollector From e64f46f8e5ada761263005a3ec43f420d873ece2 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:37:11 -0500 Subject: [PATCH 09/34] Symfony 6.4 Support (#177) * Update GitHub actions * Update to PHP 8.1 * Code cleanup * testing on Symfony 6.4 --- .github/workflows/main.yml | 65 ++++--------------- LICENSE | 2 +- composer.json | 2 +- readme.md | 4 +- src/Codeception/Lib/Connector/Symfony.php | 23 +++---- src/Codeception/Module/Symfony.php | 17 +++-- .../Module/Symfony/BrowserAssertionsTrait.php | 5 +- .../Module/Symfony/EventsAssertionsTrait.php | 4 +- .../Module/Symfony/FormAssertionsTrait.php | 1 - .../Module/Symfony/MailerAssertionsTrait.php | 17 ++--- .../Module/Symfony/MimeAssertionsTrait.php | 4 +- .../Symfony/ParameterAssertionsTrait.php | 6 +- .../Module/Symfony/RouterAssertionsTrait.php | 27 ++------ .../Symfony/SecurityAssertionsTrait.php | 4 +- .../Module/Symfony/SessionAssertionsTrait.php | 6 +- .../Module/Symfony/TimeAssertionsTrait.php | 2 +- .../Module/Symfony/TwigAssertionsTrait.php | 8 +-- 17 files changed, 59 insertions(+), 138 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 885dd616..1898a021 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,19 +8,12 @@ jobs: strategy: matrix: - php: [8.0, 8.1, 8.2] - symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*", "6.2.*", "6.3.*"] - exclude: - - php: 8.0 - symfony: "6.1.*" - - php: 8.0 - symfony: "6.2.*" - - php: 8.0 - symfony: "6.3.*" + php: [8.1, 8.2, 8.3] + symfony: ["5.4.*", "6.4.*"] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -30,67 +23,35 @@ jobs: extensions: ctype, iconv, intl, json, mbstring, pdo, pdo_sqlite coverage: none - - name: Checkout Symfony 4.4 Sample - if: "matrix.symfony == '4.4.*'" - uses: actions/checkout@v2 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "4.4_codecept5" - - name: Checkout Symfony 5.4 Sample if: "matrix.symfony == '5.4.*'" - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: Codeception/symfony-module-tests path: framework-tests ref: "5.4_codecept5" - - name: Checkout Symfony 6.0 Sample - if: "matrix.symfony == '6.0.*'" - uses: actions/checkout@v2 + - name: Checkout Symfony 6.4 Sample + if: "matrix.symfony == '6.4.*'" + uses: actions/checkout@v4 with: repository: Codeception/symfony-module-tests path: framework-tests - ref: "6.0" - - - name: Checkout Symfony 6.1 Sample - if: "matrix.symfony == '6.1.*'" - uses: actions/checkout@v2 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "6.1" - - - name: Checkout Symfony 6.2 Sample - if: "matrix.symfony == '6.2.*'" - uses: actions/checkout@v2 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "6.2" - - - name: Checkout Symfony 6.3 Sample - if: "matrix.symfony == '6.3.*'" - uses: actions/checkout@v2 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "6.3" + ref: "6.4" - name: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache composer dependencies - uses: actions/cache@v2.1.3 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- - - name: Install PHPUnit 9 for Symfony 4.4, 5.4 and 6.0 - if: "matrix.symfony == '4.4.*' || matrix.symfony == '5.4.*' || matrix.symfony == '6.0.*'" + - name: Install PHPUnit 9 for Symfony 5.4 + if: "matrix.symfony == '5.4.*'" run: composer require --dev --no-update "phpunit/phpunit=^9.0" - name: Install dependencies @@ -111,8 +72,8 @@ jobs: run: composer validate working-directory: framework-tests - - name: Install PHPUnit 10 in framework-tests for Symfony 6.1 and 6.2 - if: "matrix.symfony == '6.1.*' || matrix.symfony == '6.2.*' || matrix.symfony == '6.3.*'" + - name: Install PHPUnit 10 in framework-tests for Symfony 6.4 + if: "matrix.symfony == '6.4.*'" run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests diff --git a/LICENSE b/LICENSE index 61d82091..624026b5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2011-2020 Michael Bodnarchuk and contributors +Copyright (c) 2011-2024 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 24d5822d..52a4293b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "homepage": "https://codeception.com/", "require": { - "php": "^8.0", + "php": "^8.1", "ext-json": "*", "codeception/codeception": "^5.0.8", "codeception/lib-innerbrowser": "^3.1.1 | ^4.0" diff --git a/readme.md b/readme.md index c832d43a..49c9e0ab 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,8 @@ A Codeception module for Symfony framework. ## Requirements -* `Symfony` `4.4.x`, `5.4.x`, `6.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). -* `PHP 8.0` or higher. +* `Symfony` `5.4.x`, `6.4.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). +* `PHP 8.1` or higher. ## Installation diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index dafcaa5a..684add44 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -20,36 +20,29 @@ class Symfony extends HttpKernelBrowser { - private bool $rebootable; - private bool $hasPerformedRequest = false; private ?ContainerInterface $container; - public array $persistentServices = []; - /** * Constructor. * * @param Kernel $kernel A booted HttpKernel instance - * @param array $services An injected services - * @param bool $rebootable + * @param array $persistentServices An injected services */ - public function __construct(Kernel $kernel, array $services = [], bool $rebootable = true) - { + public function __construct( + Kernel $kernel, + public array $persistentServices = [], + private readonly bool $rebootable = true + ) { parent::__construct($kernel); $this->followRedirects(); - $this->rebootable = $rebootable; - $this->persistentServices = $services; $this->container = $this->getContainer(); $this->rebootKernel(); } - /** - * @param Request $request - * @return Response - */ - protected function doRequest($request): Response + /** @param Request $request */ + protected function doRequest(object $request): Response { if ($this->rebootable) { if ($this->hasPerformedRequest) { diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index fc8044ca..bd7b898a 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -43,7 +43,6 @@ use Symfony\Component\VarDumper\Cloner\Data; use function array_keys; use function array_map; -use function array_merge; use function array_search; use function array_unique; use function class_exists; @@ -74,7 +73,7 @@ * * ## Config * - * ### Symfony 5.x or 4.4 + * ### Symfony 5.4 or higher * * * app_path: 'src' - Specify custom path to your app dir, where the kernel interface is located. * * environment: 'local' - Environment used for load kernel @@ -83,8 +82,8 @@ * * debug: true - Turn on/off debug mode * * cache_router: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) * * rebootable_client: 'true' - Reboot client's kernel before each request - * * guard: 'false' - Enable custom authentication system with guard (only for 4.x and 5.x versions of the symfony) - * * authenticator: 'false' - Reboot client's kernel before each request (only for 6.x versions of the symfony) + * * guard: 'false' - Enable custom authentication system with guard (only for Symfony 5.4) + * * authenticator: 'false' - Reboot client's kernel before each request (only for Symfony 6.0 or higher) * * #### Example (`functional.suite.yml`) - Symfony 4 Directory Structure * @@ -126,7 +125,7 @@ * browser: firefox * ``` * - * If you're using Symfony with Eloquent ORM (instead of Doctrine), you can load the [`ORM` part of Laravel module](https://codeception.com/docs/modules/Laravel5#Parts) + * If you're using Symfony with Eloquent ORM (instead of Doctrine), you can load the [`ORM` part of Laravel module](https://codeception.com/docs/modules/Laravel#Parts) * in addition to Symfony module. * */ @@ -215,7 +214,7 @@ public function _initialize(): void */ public function _before(TestInterface $test): void { - $this->persistentServices = array_merge($this->persistentServices, $this->permanentServices); + $this->persistentServices = [...$this->persistentServices, ...$this->permanentServices]; $this->client = new SymfonyConnector($this->kernel, $this->persistentServices, $this->config['rebootable_client']); } @@ -322,7 +321,7 @@ protected function getKernelClass(): string $this->requireAdditionalAutoloader(); - $filesRealPath = array_map(function ($file) { + $filesRealPath = array_map(static function ($file) { require_once $file; return $file->getRealPath(); }, $results); @@ -331,7 +330,7 @@ protected function getKernelClass(): string if (class_exists($kernelClass)) { $reflectionClass = new ReflectionClass($kernelClass); - if ($file = array_search($reflectionClass->getFileName(), $filesRealPath)) { + if ($file = array_search($reflectionClass->getFileName(), $filesRealPath, true)) { return $kernelClass; } @@ -355,7 +354,7 @@ protected function getProfile(): ?Profile try { $response = $this->getClient()->getResponse(); return $profiler->loadProfileFromResponse($response); - } catch (BadMethodCallException $e) { + } catch (BadMethodCallException) { $this->fail('You must perform a request before using this method.'); } catch (Exception $e) { $this->fail($e->getMessage()); diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 67dc1ddb..001e7ca2 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -31,7 +31,7 @@ public function rebootClientKernel(): void /** * Verifies that a page is available. - * By default it checks the current page, specify the `$url` parameter to change it. + * By default, it checks the current page, specify the `$url` parameter to change it. * * ```php * seePageRedirectsTo('/admin', '/login'); * ``` - * - * @param string $page - * @param string $redirectsTo */ public function seePageRedirectsTo(string $page, string $redirectsTo): void { diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 0ef1ee4c..8ee296b9 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -25,7 +25,7 @@ trait EventsAssertionsTrait */ public function dontSeeEvent(array|string $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); } @@ -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..c6fba53d 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -43,7 +43,6 @@ public function dontSeeFormErrors(): void * $I->seeFormErrorMessage('username', 'Username is empty'); * ``` * - * @param string $field * @param string|null $message */ public function seeFormErrorMessage(string $field, string $message = null): void diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 4e61cb2d..f8bb9772 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -14,8 +14,7 @@ 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`. + * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; otherwise this check will *always* pass. */ public function dontSeeEmailIsSent(): void { @@ -25,8 +24,7 @@ public function dontSeeEmailIsSent(): void /** * 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`. + * If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. * * ```php * 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..12e73cd8 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -169,9 +169,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."; + $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 ?: $this->fail( - sprintf($errorMsgFormat, $function) + 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..61c98ddd 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,8 @@ trait ParameterAssertionsTrait * grabParameter('app.business_name'); * ``` - * - * @param string $parameterName - * @return array|bool|float|int|string|null */ - 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..80501555 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 { @@ -135,7 +124,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 +138,6 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void * seeInCurrentRoute('my_blog_pages'); * ``` - * - * @param string $routeName */ public function seeInCurrentRoute(string $routeName): void { diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index a07e3ab3..b86bd1ff 100644 --- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -65,7 +65,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 +108,6 @@ public function seeRememberedAuthentication(): void * seeUserHasRole('ROLE_ADMIN'); * ``` - * - * @param string $role */ public function seeUserHasRole(string $role): void { diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index a8b69afd..7d26314d 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -178,11 +178,7 @@ 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'); } 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/TwigAssertionsTrait.php b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php index 52b02d0d..1bfba3ec 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, From 3f41b870cb3ee28ce82c044a23fa09ec3e07605a Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Wed, 10 Jan 2024 20:40:44 +0100 Subject: [PATCH 10/34] Minor formating --- src/Codeception/Module/Symfony.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index bd7b898a..9f3b5e62 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -75,17 +75,17 @@ * * ### Symfony 5.4 or higher * - * * app_path: 'src' - Specify custom path to your app dir, where the kernel interface is located. - * * environment: 'local' - Environment used for load kernel - * * kernel_class: 'App\Kernel' - Kernel class name - * * em_service: 'doctrine.orm.entity_manager' - Use the stated EntityManager to pair with Doctrine Module. - * * debug: true - Turn on/off debug mode - * * cache_router: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) - * * rebootable_client: 'true' - Reboot client's kernel before each request - * * guard: 'false' - Enable custom authentication system with guard (only for Symfony 5.4) - * * authenticator: 'false' - Reboot client's kernel before each request (only for Symfony 6.0 or higher) + * * `app_path`: 'src' - Specify custom path to your app dir, where the kernel interface is located. + * * `environment`: 'local' - Environment used for load kernel + * * `kernel_class`: 'App\Kernel' - Kernel class name + * * `em_service`: 'doctrine.orm.entity_manager' - Use the stated EntityManager to pair with Doctrine Module. + * * `debug`: true - Turn on/off [debug mode](https://codeception.com/docs/Debugging) + * * `cache_router`: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) + * * `rebootable_client`: 'true' - Reboot client's kernel before each request + * * `guard`: 'false' - Enable custom authentication system with guard (only for Symfony 5.4) + * * `authenticator`: 'false' - Reboot client's kernel before each request (only for Symfony 6.0 or higher) * - * #### Example (`functional.suite.yml`) - Symfony 4 Directory Structure + * #### Sample `Functional.suite.yml` * * modules: * enabled: From b887260336680c0d5080b1ae5fd65334e9643618 Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Thu, 11 Jan 2024 00:28:58 +0100 Subject: [PATCH 11/34] Fix return type of grabSecurityService (#178) Since symfony 6.2 the "security.helper" service is an instance of Symfony\Bundle\SecurityBundle\Security --- src/Codeception/Module/Symfony/SecurityAssertionsTrait.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index b86bd1ff..afd160bf 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; @@ -176,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'); } From 8b4b5333eb5a48f2d8fddc8be1e70e9d7731320b Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Wed, 10 Jan 2024 22:29:01 -0500 Subject: [PATCH 12/34] Reference the correct Cest files in Contributing.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3624a43..e2eb5a9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ It is a minimal (but complete) Symfony project, ready to run tests.
- Edit the trait's source code in the `vendor/codeception/module-symfony/src/Codeception/Module/Symfony/` folder.
-- If you create a new method, you can test it by adding a test in the `tests/Functional/SymfonyModuleCest.php` file. +- If you create a new method, you can test it by adding a test in the corresponding `/tests/Functional/*Cest.php` file. > :bulb: Be sure to rebuild Codeception's "Actor" classes (see [Console Commands](https://codeception.com/docs/reference/Commands#Build)): > ```shell > vendor/bin/codecept clean From be24b2b64759e5b4dd83ca091ecfe49c26cecc5c Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Thu, 11 Jan 2024 00:45:50 -0500 Subject: [PATCH 13/34] Test on Symfony 7.0 (#180) --- .github/workflows/main.yml | 23 ++++++++++++++----- composer.json | 46 +++++++++++++++++++------------------- readme.md | 2 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1898a021..cf694cb1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,10 @@ jobs: strategy: matrix: php: [8.1, 8.2, 8.3] - symfony: ["5.4.*", "6.4.*"] + symfony: ["5.4.*", "6.4.*", "7.0.*"] + exclude: + - php: 8.1 + symfony: "7.0.*" steps: - name: Checkout code @@ -29,7 +32,7 @@ jobs: with: repository: Codeception/symfony-module-tests path: framework-tests - ref: "5.4_codecept5" + ref: "5.4" - name: Checkout Symfony 6.4 Sample if: "matrix.symfony == '6.4.*'" @@ -39,6 +42,14 @@ jobs: path: framework-tests ref: "6.4" + - name: Checkout Symfony 7.0 Sample + if: "matrix.symfony == '7.0.*'" + uses: actions/checkout@v4 + with: + repository: Codeception/symfony-module-tests + path: framework-tests + ref: "7.0" + - name: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" @@ -72,8 +83,8 @@ jobs: run: composer validate working-directory: framework-tests - - name: Install PHPUnit 10 in framework-tests for Symfony 6.4 - if: "matrix.symfony == '6.4.*'" + - name: Install PHPUnit 10 in framework-tests for Symfony 6.4 and 7.0 + if: "matrix.symfony == '6.4.*' || matrix.symfony == '7.0.*'" run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests @@ -85,8 +96,8 @@ jobs: - name: Prepare the test environment run: | - php bin/console d:s:u -f - php bin/console d:f:l -q + php bin/console doctrine:schema:update --force + php bin/console doctrine:fixtures:load --quiet working-directory: framework-tests - name: Run test suite diff --git a/composer.json b/composer.json index 52a4293b..5e148b68 100644 --- a/composer.json +++ b/composer.json @@ -27,29 +27,29 @@ "codeception/module-asserts": "^3.0", "codeception/module-doctrine2": "^3.0", "doctrine/orm": "^2.10", - "symfony/browser-kit": "^4.4 | ^5.0 | ^6.0", - "symfony/cache": "^4.4 | ^5.0 | ^6.0", - "symfony/config": "^4.4 | ^5.0 | ^6.0", - "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0", - "symfony/dom-crawler": "^4.4 | ^5.0 | ^6.0", - "symfony/error-handler": "^4.4 | ^5.0 | ^6.0", - "symfony/filesystem": "^4.4 | ^5.0 | ^6.0", - "symfony/form": "^4.4 | ^5.0 | ^6.0", - "symfony/framework-bundle": "^4.4 | ^5.0 | ^6.0", - "symfony/http-foundation": "^4.4 | ^5.0 | ^6.0", - "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0", - "symfony/mailer": "^4.4 | ^5.0 | ^6.0", - "symfony/mime": "^4.4 | ^5.0 | ^6.0", - "symfony/options-resolver": "^4.4 | ^5.0 | ^6.0", - "symfony/property-access": "^4.4 | ^5.0 | ^6.0", - "symfony/property-info": "^4.4 | ^5.0 | ^6.0", - "symfony/routing": "^4.4 | ^5.0 | ^6.0", - "symfony/security-bundle": "^4.4 | ^5.0 | ^6.0", - "symfony/security-core": "^4.4 | ^5.0 | ^6.0", - "symfony/security-csrf": "^4.4 | ^5.0 | ^6.0", - "symfony/security-http": "^4.4 | ^5.0 | ^6.0", - "symfony/twig-bundle": "^4.4 | ^5.0 | ^6.0", - "symfony/var-exporter": "^4.4 | ^5.0 | ^6.0", + "symfony/browser-kit": "^5.4 | ^6.4 | ^7.0", + "symfony/cache": "^5.4 | ^6.4 | ^7.0", + "symfony/config": "^5.4 | ^6.4 | ^7.0", + "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.0", + "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.0", + "symfony/error-handler": "^5.4 | ^6.4 | ^7.0", + "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-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/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", + "symfony/routing": "^5.4 | ^6.4 | ^7.0", + "symfony/security-bundle": "^5.4 | ^6.4 | ^7.0", + "symfony/security-core": "^5.4 | ^6.4 | ^7.0", + "symfony/security-csrf": "^5.4 | ^6.4 | ^7.0", + "symfony/security-http": "^5.4 | ^6.4 | ^7.0", + "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.0", + "symfony/var-exporter": "^5.4 | ^6.4 | ^7.0", "vlucas/phpdotenv": "^4.2 | ^5.4" }, "suggest": { diff --git a/readme.md b/readme.md index 49c9e0ab..06d2d614 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ A Codeception module for Symfony framework. ## Requirements -* `Symfony` `5.4.x`, `6.4.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). +* `Symfony` `5.4.x`, `6.4.x`, `7.0.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). * `PHP 8.1` or higher. ## Installation From e3fd1e43b1df767b1a372c7602e5e0f1c104ccb2 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:12:08 -0500 Subject: [PATCH 14/34] Fix CI --- .github/workflows/main.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf694cb1..44477a5b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,9 +61,8 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- - - name: Install PHPUnit 9 for Symfony 5.4 - if: "matrix.symfony == '5.4.*'" - run: composer require --dev --no-update "phpunit/phpunit=^9.0" + - name: Install PHPUnit 10 + run: composer require --dev --no-update "phpunit/phpunit=^10.0" - name: Install dependencies run: | @@ -76,21 +75,20 @@ jobs: composer require symfony/browser-kit=${{ matrix.symfony }} --no-update composer require vlucas/phpdotenv --no-update composer require codeception/module-asserts="3.*" --no-update - composer require codeception/module-doctrine2="3.*" --no-update + composer require codeception/module-doctrine="3.*" --no-update composer update --prefer-dist --no-progress --no-dev - name: Validate composer.json and composer.lock run: composer validate working-directory: framework-tests - - name: Install PHPUnit 10 in framework-tests for Symfony 6.4 and 7.0 - if: "matrix.symfony == '6.4.*' || matrix.symfony == '7.0.*'" - run: composer require --dev --no-update "phpunit/phpunit=^10.0" + - name: Install PHPUnit 10 in framework-tests + run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests - name: Install Symfony Sample run: | - composer remove codeception/codeception codeception/module-asserts codeception/module-doctrine2 codeception/lib-innerbrowser codeception/module-symfony --dev --no-update + composer remove codeception/codeception codeception/module-asserts codeception/module-doctrine codeception/lib-innerbrowser codeception/module-symfony --dev --no-update composer update --no-progress working-directory: framework-tests From 666c35c8647d3d7500428aa09213cbe763b2aed6 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Tue, 12 Mar 2024 03:24:32 +0100 Subject: [PATCH 15/34] Renaming Doctrine2 to Doctrine (#184) * Renaming Doctrine2 -> Doctrine Co-authored-by: Dieter Beck --- composer.json | 2 +- src/Codeception/Module/Symfony.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5e148b68..1421734d 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ }, "require-dev": { "codeception/module-asserts": "^3.0", - "codeception/module-doctrine2": "^3.0", + "codeception/module-doctrine": "^3.1", "doctrine/orm": "^2.10", "symfony/browser-kit": "^5.4 | ^6.4 | ^7.0", "symfony/cache": "^5.4 | ^6.4 | ^7.0", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 9f3b5e62..13a9f6df 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -62,7 +62,7 @@ * and [HttpKernel Component](https://symfony.com/doc/current/components/http_kernel.html) to emulate requests and test response. * * * Access Symfony services through the dependency injection container: [`$I->grabService(...)`](#grabService) - * * Use Doctrine to test against the database: `$I->seeInRepository(...)` - see [Doctrine Module](https://codeception.com/docs/modules/Doctrine2) + * * Use Doctrine to test against the database: `$I->seeInRepository(...)` - see [Doctrine Module](https://codeception.com/docs/modules/Doctrine) * * Assert that emails would have been sent: [`$I->seeEmailIsSent()`](#seeEmailIsSent) * * Tests are wrapped into Doctrine transaction to speed them up. * * Symfony Router can be cached between requests to speed up testing. @@ -118,7 +118,7 @@ * enabled: * - Symfony: * part: services - * - Doctrine2: + * - Doctrine: * depends: Symfony * - WebDriver: * url: http://example.com From e45d5af6ea7714d5d53acac33df48781b23afd2d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 15 Mar 2024 18:44:12 +0000 Subject: [PATCH 16/34] Ignore fragments when checking routes (#185) --- src/Codeception/Module/Symfony/RouterAssertionsTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index 80501555..699b23b1 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -117,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); @@ -147,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']; @@ -161,4 +163,4 @@ protected function grabRouterService(): RouterInterface { return $this->grabService('router'); } -} \ No newline at end of file +} From 3bbf45cffb6c0bed16b088e48ba902c66a342a32 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:33:41 -0500 Subject: [PATCH 17/34] Fix: runSymfonyConsoleCommand ignores specific options (#188) --- .../Module/Symfony/ConsoleAssertionsTrait.php | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php index 66edec9e..18e20173 100644 --- a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php @@ -5,6 +5,7 @@ namespace Codeception\Module\Symfony; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\HttpKernel\KernelInterface; @@ -33,8 +34,10 @@ public function runSymfonyConsoleCommand(string $command, array $parameters = [] $commandTester = new CommandTester($consoleCommand); $commandTester->setInputs($consoleInputs); - $parameters = ['command' => $command] + $parameters; - $exitCode = $commandTester->execute($parameters); + $input = ['command' => $command] + $parameters; + $options = $this->configureOptions($parameters); + + $exitCode = $commandTester->execute($input, $options); $output = $commandTester->getDisplay(); $this->assertSame( @@ -51,6 +54,49 @@ public function runSymfonyConsoleCommand(string $command, array $parameters = [] return $output; } + 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'); From 36e08c97c0cf69085c4f185349b6197ce3c8c10a Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:46:35 -0500 Subject: [PATCH 18/34] Added Symfony validator assertions (#189) --- composer.json | 1 + src/Codeception/Module/Symfony.php | 2 + .../Symfony/ValidatorAssertionsTrait.php | 106 ++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php diff --git a/composer.json b/composer.json index 1421734d..93757670 100644 --- a/composer.json +++ b/composer.json @@ -49,6 +49,7 @@ "symfony/security-csrf": "^5.4 | ^6.4 | ^7.0", "symfony/security-http": "^5.4 | ^6.4 | ^7.0", "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.0", + "symfony/validator": "^5.4 | ^6.4 | ^7.0", "symfony/var-exporter": "^5.4 | ^6.4 | ^7.0", "vlucas/phpdotenv": "^4.2 | ^5.4" }, diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 13a9f6df..2e6f4d74 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -24,6 +24,7 @@ use Codeception\Module\Symfony\SessionAssertionsTrait; use Codeception\Module\Symfony\TimeAssertionsTrait; use Codeception\Module\Symfony\TwigAssertionsTrait; +use Codeception\Module\Symfony\ValidatorAssertionsTrait; use Codeception\TestInterface; use Doctrine\ORM\EntityManagerInterface; use Exception; @@ -145,6 +146,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use SessionAssertionsTrait; use TimeAssertionsTrait; use TwigAssertionsTrait; + use ValidatorAssertionsTrait; public Kernel $kernel; diff --git a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php new file mode 100644 index 00000000..ca82e196 --- /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(mixed $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(mixed $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, mixed $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, mixed $subject, string $propertyPath): void + { + $violations = $this->getViolationsForSubject($subject, $propertyPath); + $containsExpected = false; + foreach ($violations as $violation) { + if ($violation->getPropertyPath() === $propertyPath && str_contains($violation->getMessage(), $expected)) { + $containsExpected = true; + break; + } + } + + $this->assertTrue($containsExpected, 'The violation messages do not contain: ' . $expected); + } + + /** @return ConstraintViolationInterface[] */ + protected function getViolationsForSubject(mixed $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_filter( + $violations, + static fn($violation): bool => $violation->getConstraint()::class === $constraint && + ($propertyPath === null || $violation->getPropertyPath() === $propertyPath) + ); + } + + return $violations; + } + + protected function getValidatorService(): ValidatorInterface + { + return $this->grabService(ValidatorInterface::class); + } +} From e02ff5a4d0dde327aa3caf5e1e2e28fa075871cd Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:09:08 -0500 Subject: [PATCH 19/34] Require bootstrap.php if exists, to load all necessary .env files (#190) * Require bootstrap.php if exists * add bootstrap config parameter --- composer.json | 1 + src/Codeception/Module/Symfony.php | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/composer.json b/composer.json index 93757670..684c23c1 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "symfony/config": "^5.4 | ^6.4 | ^7.0", "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.0", "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.0", + "symfony/dotenv": "^5.4 | ^6.4 | ^7.0", "symfony/error-handler": "^5.4 | ^6.4 | ^7.0", "symfony/filesystem": "^5.4 | ^6.4 | ^7.0", "symfony/form": "^5.4 | ^6.4 | ^7.0", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 2e6f4d74..6323e523 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -28,11 +28,13 @@ use Codeception\TestInterface; use Doctrine\ORM\EntityManagerInterface; use Exception; +use LogicException; use ReflectionClass; use ReflectionException; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; @@ -84,6 +86,7 @@ * * `cache_router`: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) * * `rebootable_client`: 'true' - Reboot client's kernel before each request * * `guard`: 'false' - Enable custom authentication system with guard (only for Symfony 5.4) + * * `bootstrap`: 'false' - Enable the test environment setup with the tests/bootstrap.php file if it exists or with Symfony DotEnv otherwise. If false, it does nothing. * * `authenticator`: 'false' - Reboot client's kernel before each request (only for Symfony 6.0 or higher) * * #### Sample `Functional.suite.yml` @@ -167,6 +170,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule 'em_service' => 'doctrine.orm.entity_manager', 'rebootable_client' => true, 'authenticator' => false, + 'bootstrap' => false, 'guard' => false ]; @@ -204,6 +208,9 @@ public function _initialize(): void } $this->kernel = new $this->kernelClass($this->config['environment'], $this->config['debug']); + if($this->config['bootstrap']) { + $this->bootstrapEnvironment(); + } $this->kernel->boot(); if ($this->config['cache_router'] === true) { @@ -459,6 +466,26 @@ protected function getInternalDomains(): array return array_unique($internalDomains); } + private function bootstrapEnvironment(): void + { + $bootstrapFile = $this->kernel->getProjectDir() . '/tests/bootstrap.php'; + + if (file_exists($bootstrapFile)) { + require_once $bootstrapFile; + } else { + if (!method_exists(Dotenv::class, 'bootEnv')) { + throw new LogicException( + "Symfony DotEnv is missing. Try running 'composer require symfony/dotenv'\n" . + "If you can't install DotEnv add your env files to the 'params' key in codeception.yml\n" . + "or update your symfony/framework-bundle recipe by running:\n" . + 'composer recipes:install symfony/framework-bundle --force' + ); + } + $_ENV['APP_ENV'] = $this->config['environment']; + (new Dotenv())->bootEnv('.env'); + } + } + /** * Ensures autoloader loading of additional directories. * It is only required for CI jobs to run correctly. From 5ef40f667c0db8d5ffc6895c7053756e30a851b0 Mon Sep 17 00:00:00 2001 From: Holger Date: Thu, 2 May 2024 16:12:23 +0200 Subject: [PATCH 20/34] #165 login token (#182) * Login with token * Update token logic --------- Co-authored-by: TavoNiievez --- .../Module/Symfony/SessionAssertionsTrait.php | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index 7d26314d..47e40fc3 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; @@ -32,12 +37,20 @@ trait SessionAssertionsTrait */ 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,6 +187,11 @@ 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(); @@ -194,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); } } From 1108e3ec4f7992d9000eb69e5745b12c53f77db8 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Wed, 5 Jun 2024 05:04:36 +0200 Subject: [PATCH 21/34] Update ParameterAssertionsTrait.php: Adding info about `bind` (#193) --- src/Codeception/Module/Symfony/ParameterAssertionsTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php index 61c98ddd..ecbbbbc7 100644 --- a/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ParameterAssertionsTrait.php @@ -16,6 +16,7 @@ trait ParameterAssertionsTrait * grabParameter('app.business_name'); * ``` + * This only works for explicitly set parameters (just using `bind` for Symfony's dependency injection is not enough). */ public function grabParameter(string $parameterName): array|bool|string|int|float|UnitEnum|null { From 27e94c1e00b9c12a3f38d480eb7ab619e9440bb8 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 9 Jun 2024 08:24:48 -0500 Subject: [PATCH 22/34] Add tests for Symfony 7.1 (#194) --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 44477a5b..db3651cb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,10 @@ jobs: strategy: matrix: php: [8.1, 8.2, 8.3] - symfony: ["5.4.*", "6.4.*", "7.0.*"] + symfony: ["5.4.*", "6.4.*", "7.1.*"] exclude: - php: 8.1 - symfony: "7.0.*" + symfony: "7.1.*" steps: - name: Checkout code @@ -42,13 +42,13 @@ jobs: path: framework-tests ref: "6.4" - - name: Checkout Symfony 7.0 Sample - if: "matrix.symfony == '7.0.*'" + - name: Checkout Symfony 7.1 Sample + if: "matrix.symfony == '7.1.*'" uses: actions/checkout@v4 with: repository: Codeception/symfony-module-tests path: framework-tests - ref: "7.0" + ref: "7.1" - name: Get composer cache directory id: composer-cache From 39293eaad322a2a25bafec42a2dc6384ad37aed9 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Wed, 19 Jun 2024 02:20:14 +0300 Subject: [PATCH 23/34] cache_router_doc_enhancement: fixed (#195) --- src/Codeception/Module/Symfony.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 6323e523..45b798dc 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -83,7 +83,7 @@ * * `kernel_class`: 'App\Kernel' - Kernel class name * * `em_service`: 'doctrine.orm.entity_manager' - Use the stated EntityManager to pair with Doctrine Module. * * `debug`: true - Turn on/off [debug mode](https://codeception.com/docs/Debugging) - * * `cache_router`: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) + * * `cache_router`: 'false' - Enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) (can have an impact on ajax requests sending via '$I->sendAjaxPostRequest()') * * `rebootable_client`: 'true' - Reboot client's kernel before each request * * `guard`: 'false' - Enable custom authentication system with guard (only for Symfony 5.4) * * `bootstrap`: 'false' - Enable the test environment setup with the tests/bootstrap.php file if it exists or with Symfony DotEnv otherwise. If false, it does nothing. From df4c02cb255b079c38561d0fcb898956dea69853 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Thu, 27 Jun 2024 23:14:16 -0500 Subject: [PATCH 24/34] Simplify module logic (#196) --- src/Codeception/Lib/Connector/Symfony.php | 35 +--- src/Codeception/Module/Symfony.php | 197 ++++++++-------------- 2 files changed, 84 insertions(+), 148 deletions(-) diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index 684add44..44d7595a 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -21,15 +21,8 @@ class Symfony extends HttpKernelBrowser { private bool $hasPerformedRequest = false; - private ?ContainerInterface $container; - /** - * Constructor. - * - * @param Kernel $kernel A booted HttpKernel instance - * @param array $persistentServices An injected services - */ public function __construct( Kernel $kernel, public array $persistentServices = [], @@ -74,14 +67,12 @@ public function rebootKernel(): void $this->persistDoctrineConnections(); $this->kernel->reboot(null); - $this->container = $this->getContainer(); foreach ($this->persistentServices as $serviceName => $service) { try { $this->container->set($serviceName, $service); } catch (InvalidArgumentException $e) { - //Private services can't be set in Symfony 4 codecept_debug("[Symfony] Can't set persistent service {$serviceName}: " . $e->getMessage()); } } @@ -95,31 +86,23 @@ private function getContainer(): ?ContainerInterface { /** @var ContainerInterface $container */ $container = $this->kernel->getContainer(); - if ($container->has('test.service_container')) { - $container = $container->get('test.service_container'); - } - - return $container; + return $container->has('test.service_container') + ? $container->get('test.service_container') + : $container; } private function getProfiler(): ?Profiler { - if ($this->container->has('profiler')) { - /** @var Profiler $profiler */ - $profiler = $this->container->get('profiler'); - return $profiler; - } - - return null; + return $this->container->has('profiler') + ? $this->container->get('profiler') + : null; } private function getService(string $serviceName): ?object { - if ($this->container->has($serviceName)) { - return $this->container->get($serviceName); - } - - return null; + return $this->container->has($serviceName) + ? $this->container->get($serviceName) + : null; } private function persistDoctrineConnections(): void diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 45b798dc..b677c039 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -42,11 +42,9 @@ use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Mailer\DataCollector\MessageDataCollector; -use Symfony\Component\Routing\Route; use Symfony\Component\VarDumper\Cloner\Data; use function array_keys; use function array_map; -use function array_search; use function array_unique; use function class_exists; use function codecept_root_dir; @@ -55,7 +53,6 @@ use function implode; use function ini_get; use function ini_set; -use function is_null; use function iterator_to_array; use function number_format; use function sprintf; @@ -174,46 +171,34 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule 'guard' => false ]; - /** - * @return string[] - */ - public function _parts(): array - { - return ['services']; - } - protected ?string $kernelClass = null; - /** * Services that should be persistent permanently for all tests - * - * @var array */ - protected $permanentServices = []; - + protected array $permanentServices = []; /** * Services that should be persistent during test execution between kernel reboots - * - * @var array */ - protected $persistentServices = []; + protected array $persistentServices = []; + + /** + * @return string[] + */ + public function _parts(): array + { + return ['services']; + } public function _initialize(): void { $this->kernelClass = $this->getKernelClass(); - $maxNestingLevel = 200; // Symfony may have very long nesting level - $xdebugMaxLevelKey = 'xdebug.max_nesting_level'; - if (ini_get($xdebugMaxLevelKey) < $maxNestingLevel) { - ini_set($xdebugMaxLevelKey, (string)$maxNestingLevel); - } - + $this->setXdebugMaxNestingLevel(200); $this->kernel = new $this->kernelClass($this->config['environment'], $this->config['debug']); - if($this->config['bootstrap']) { + if ($this->config['bootstrap']) { $this->bootstrapEnvironment(); } $this->kernel->boot(); - - if ($this->config['cache_router'] === true) { + if ($this->config['cache_router']) { $this->persistPermanentService('router'); } } @@ -223,7 +208,7 @@ public function _initialize(): void */ public function _before(TestInterface $test): void { - $this->persistentServices = [...$this->persistentServices, ...$this->permanentServices]; + $this->persistentServices = array_merge($this->persistentServices, $this->permanentServices); $this->client = new SymfonyConnector($this->kernel, $this->persistentServices, $this->config['rebootable_client']); } @@ -235,7 +220,6 @@ public function _after(TestInterface $test): void foreach (array_keys($this->permanentServices) as $serviceName) { $this->permanentServices[$serviceName] = $this->grabService($serviceName); } - parent::_after($test); } @@ -258,40 +242,24 @@ public function _getEntityManager(): EntityManagerInterface $emService = $this->config['em_service']; if (!isset($this->permanentServices[$emService])) { - // Try to persist configured entity manager $this->persistPermanentService($emService); $container = $this->_getContainer(); - if ($container->has('doctrine')) { - $this->persistPermanentService('doctrine'); - } - - if ($container->has('doctrine.orm.default_entity_manager')) { - $this->persistPermanentService('doctrine.orm.default_entity_manager'); - } - - if ($container->has('doctrine.dbal.default_connection')) { - $this->persistPermanentService('doctrine.dbal.default_connection'); + $services = ['doctrine', 'doctrine.orm.default_entity_manager', 'doctrine.dbal.default_connection']; + foreach ($services as $service) { + if ($container->has($service)) { + $this->persistPermanentService($service); + } } } return $this->permanentServices[$emService]; } - /** - * Return container. - */ public function _getContainer(): ContainerInterface { $container = $this->kernel->getContainer(); - if (!$container instanceof ContainerInterface) { - $this->fail('Could not get Symfony container'); - } - if ($container->has('test.service_container')) { - $container = $container->get('test.service_container'); - } - - return $container; + return $container->has('test.service_container') ? $container->get('test.service_container') : $container; } protected function getClient(): SymfonyConnector @@ -317,9 +285,10 @@ protected function getKernelClass(): string ); } + $this->requireAdditionalAutoloader(); + $finder = new Finder(); - $finder->name('*Kernel.php')->depth('0')->in($path); - $results = iterator_to_array($finder); + $results = iterator_to_array($finder->name('*Kernel.php')->depth('0')->in($path)); if ($results === []) { throw new ModuleRequireException( self::class, @@ -328,22 +297,17 @@ protected function getKernelClass(): string ); } - $this->requireAdditionalAutoloader(); - + $kernelClass = $this->config['kernel_class']; $filesRealPath = array_map(static function ($file) { require_once $file; return $file->getRealPath(); }, $results); - $kernelClass = $this->config['kernel_class']; - if (class_exists($kernelClass)) { $reflectionClass = new ReflectionClass($kernelClass); - if ($file = array_search($reflectionClass->getFileName(), $filesRealPath, true)) { + if (in_array($reflectionClass->getFileName(), $filesRealPath, true)) { return $kernelClass; } - - throw new ModuleRequireException(self::class, "Kernel class was not found in {$file}."); } throw new ModuleRequireException( @@ -356,13 +320,9 @@ protected function getKernelClass(): string protected function getProfile(): ?Profile { /** @var Profiler $profiler */ - if (!$profiler = $this->getService('profiler')) { - return null; - } - + $profiler = $this->getService('profiler'); try { - $response = $this->getClient()->getResponse(); - return $profiler->loadProfileFromResponse($response); + return $profiler?->loadProfileFromResponse($this->getClient()->getResponse()); } catch (BadMethodCallException) { $this->fail('You must perform a request before using this method.'); } catch (Exception $e) { @@ -377,20 +337,12 @@ protected function getProfile(): ?Profile */ protected function grabCollector(string $collector, string $function, string $message = null): DataCollectorInterface { - if (($profile = $this->getProfile()) === null) { - $this->fail( - sprintf("The Profile is needed to use the '%s' function.", $function) - ); + $profile = $this->getProfile(); + if ($profile === null) { + $this->fail(sprintf("The Profile is needed to use the '%s' function.", $function)); } - if (!$profile->hasCollector($collector)) { - if ($message) { - $this->fail($message); - } - - $this->fail( - sprintf("The '%s' collector is needed to use the '%s' function.", $collector, $function) - ); + $this->fail($message ?: "The '{$collector}' collector is needed to use the '{$function}' function."); } return $profile->getCollector($collector); @@ -399,49 +351,23 @@ protected function grabCollector(string $collector, string $function, string $me /** * Set the data that will be displayed when running a test with the `--debug` flag * - * @param $url + * @param mixed $url */ protected function debugResponse($url): void { parent::debugResponse($url); - - if (($profile = $this->getProfile()) === null) { - return; - } - - if ($profile->hasCollector('security')) { - /** @var SecurityDataCollector $security */ - $security = $profile->getCollector('security'); - if ($security->isAuthenticated()) { - $roles = $security->getRoles(); - - if ($roles instanceof Data) { - $roles = $roles->getValue(); + if ($profile = $this->getProfile()) { + $collectors = [ + 'security' => 'debugSecurityData', + 'mailer' => 'debugMailerData', + 'time' => 'debugTimeData', + ]; + foreach ($collectors as $collector => $method) { + if ($profile->hasCollector($collector)) { + $this->$method($profile->getCollector($collector)); } - - $this->debugSection( - 'User', - $security->getUser() - . ' [' . implode(',', $roles) . ']' - ); - } else { - $this->debugSection('User', 'Anonymous'); } } - - if ($profile->hasCollector('mailer')) { - /** @var MessageDataCollector $mailerCollector */ - $mailerCollector = $profile->getCollector('mailer'); - $emails = count($mailerCollector->getEvents()->getMessages()); - $this->debugSection('Emails', $emails . ' sent'); - } - - if ($profile->hasCollector('time')) { - /** @var TimeDataCollector $timeCollector */ - $timeCollector = $profile->getCollector('time'); - $duration = number_format($timeCollector->getDuration(), 2) . ' ms'; - $this->debugSection('Time', $duration); - } } /** @@ -450,15 +376,14 @@ protected function debugResponse($url): void protected function getInternalDomains(): array { $internalDomains = []; - $router = $this->grabRouterService(); $routes = $router->getRouteCollection(); - /* @var Route $route */ + foreach ($routes as $route) { - if (!is_null($route->getHost())) { - $compiled = $route->compile(); - if (!is_null($compiled->getHostRegex())) { - $internalDomains[] = $compiled->getHostRegex(); + if ($route->getHost() !== null) { + $compiledRoute = $route->compile(); + if ($compiledRoute->getHostRegex() !== null) { + $internalDomains[] = $compiledRoute->getHostRegex(); } } } @@ -466,10 +391,16 @@ protected function getInternalDomains(): array return array_unique($internalDomains); } + private function setXdebugMaxNestingLevel(int $maxNestingLevel): void + { + if (ini_get('xdebug.max_nesting_level') < $maxNestingLevel) { + ini_set('xdebug.max_nesting_level', (string)$maxNestingLevel); + } + } + private function bootstrapEnvironment(): void { $bootstrapFile = $this->kernel->getProjectDir() . '/tests/bootstrap.php'; - if (file_exists($bootstrapFile)) { require_once $bootstrapFile; } else { @@ -486,6 +417,28 @@ private function bootstrapEnvironment(): void } } + private function debugSecurityData(SecurityDataCollector $security): void + { + if ($security->isAuthenticated()) { + $roles = $security->getRoles(); + $rolesString = implode(',', $roles instanceof Data ? $roles->getValue() : $roles); + $userInfo = $security->getUser() . ' [' . $rolesString . ']'; + } else { + $userInfo = 'Anonymous'; + } + $this->debugSection('User', $userInfo); + } + + private function debugMailerData(MessageDataCollector $mailerCollector): void + { + $this->debugSection('Emails', count($mailerCollector->getEvents()->getMessages()) . ' sent'); + } + + private function debugTimeData(TimeDataCollector $timeCollector): void + { + $this->debugSection('Time', number_format($timeCollector->getDuration(), 2) . ' ms'); + } + /** * Ensures autoloader loading of additional directories. * It is only required for CI jobs to run correctly. From 9b77251bc2c2a3d4d84eee84da1d971127c1ef4c Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Fri, 26 Jul 2024 17:22:48 +0200 Subject: [PATCH 25/34] Declare nullable parameter types explicitly for PHP 8.4 compatibility (#197) --- src/Codeception/Module/Symfony.php | 2 +- .../Module/Symfony/BrowserAssertionsTrait.php | 2 +- .../Module/Symfony/EventsAssertionsTrait.php | 4 ++-- .../Module/Symfony/FormAssertionsTrait.php | 2 +- .../Module/Symfony/MimeAssertionsTrait.php | 20 +++++++++---------- .../Symfony/SecurityAssertionsTrait.php | 2 +- .../Module/Symfony/SessionAssertionsTrait.php | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index b677c039..c508d5e3 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -335,7 +335,7 @@ protected function getProfile(): ?Profile /** * Grabs a Symfony Data Collector */ - protected function grabCollector(string $collector, string $function, string $message = null): DataCollectorInterface + protected function grabCollector(string $collector, string $function, ?string $message = null): DataCollectorInterface { $profile = $this->getProfile(); if ($profile === null) { diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 001e7ca2..9eb56364 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -43,7 +43,7 @@ public function rebootClientKernel(): void * * @param string|null $url */ - public function seePageIsAvailable(string $url = null): void + public function seePageIsAvailable(?string $url = null): void { if ($url !== null) { $this->amOnPage($url); diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 8ee296b9..f761499b 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -23,7 +23,7 @@ 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_column($this->getCalledListeners(), 'event')]; $actual = [$this->getOrphanedEvents(), $actualEvents]; @@ -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); diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index c6fba53d..930969c1 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -45,7 +45,7 @@ public function dontSeeFormErrors(): void * * @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__); diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index 12e73cd8..d20ea306 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -20,7 +20,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 +35,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 +50,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 +66,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 +82,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 +97,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 +112,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 +127,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 +142,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 +157,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))); diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index afd160bf..81559730 100644 --- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -163,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(); diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index 47e40fc3..aa7ac9e9 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -35,13 +35,13 @@ 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 { $token = $this->createAuthenticationToken($user, $firewallName); $this->loginWithToken($token, $firewallName, $firewallContext); } - public function amLoggedInWithToken(TokenInterface $token, string $firewallName = 'main', string $firewallContext = null): void + public function amLoggedInWithToken(TokenInterface $token, string $firewallName = 'main', ?string $firewallContext = null): void { $this->loginWithToken($token, $firewallName, $firewallContext); } From 72cf4d1ba74627b870d6b38ed4e0807cc85819d3 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sat, 17 Aug 2024 21:25:18 -0500 Subject: [PATCH 26/34] Inherit symfony pre-built assertions (#198) --- composer.json | 3 + src/Codeception/Module/Symfony.php | 6 + .../Module/Symfony/BrowserAssertionsTrait.php | 195 +++++++++++++++++- .../Symfony/DomCrawlerAssertionsTrait.php | 176 ++++++++++++++++ .../Module/Symfony/FormAssertionsTrait.php | 25 ++- .../Symfony/HttpClientAssertionsTrait.php | 117 +++++++++++ .../Module/Symfony/MailerAssertionsTrait.php | 68 ++++-- .../Module/Symfony/MimeAssertionsTrait.php | 17 ++ .../Symfony/NotificationAssertionsTrait.php | 91 ++++++++ 9 files changed, 679 insertions(+), 19 deletions(-) create mode 100644 src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php create mode 100644 src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php create mode 100644 src/Codeception/Module/Symfony/NotificationAssertionsTrait.php diff --git a/composer.json b/composer.json index 684c23c1..0bdc3bd6 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 c508d5e3..34694898 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 9eb56364..cc8bfb54 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -4,11 +4,194 @@ 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 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 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); + } + + /** + * 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 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 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 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 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 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 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 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 that the response was successful (HTTP status is 2xx). + */ + public function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void + { + $this->assertThatForResponse(new ResponseIsSuccessful($verbose), $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 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 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 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 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 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 +233,7 @@ public function seePageIsAvailable(?string $url = null): void $this->seeInCurrentUrl($url); } - $this->assertThat($this->getClient()->getResponse(), new ResponseIsSuccessful()); + $this->assertResponseIsSuccessful(); } /** @@ -104,4 +287,14 @@ public function submitSymfonyForm(string $name, array $fields): void $this->submitForm($selector, $params, $button); } + + protected function assertThatForClient(Constraint $constraint, string $message = ''): void + { + $this->assertThat($this->getClient(), $constraint, $message); + } + + 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 00000000..0d5bca59 --- /dev/null +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -0,0 +1,176 @@ +assertThat($this->getCrawler(), LogicalAnd::fromConstraints( + new CrawlerSelectorExists($selector), + new CrawlerAnySelectorTextContains($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 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 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 + ); + } + + /** + * 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 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 the `` element contains the given title. + */ + public function assertPageTitleContains(string $expectedTitle, string $message = ''): void + { + $this->assertSelectorTextContains('title', $expectedTitle, $message); + } + + /** + * Asserts that the `<title>` element is equal to the given title. + */ + public function assertPageTitleSame(string $expectedTitle, string $message = ''): void + { + $this->assertSelectorTextSame('title', $expectedTitle, $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 the given selector does match at least one element in the response. + */ + public function assertSelectorExists(string $selector, string $message = ''): void + { + $this->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 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 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 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); + } + + 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 930969c1..0c8736f0 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -12,6 +12,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. * @@ -42,8 +65,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 { diff --git a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php new file mode 100644 index 00000000..9ac3a6e4 --- /dev/null +++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); + +namespace Codeception\Module\Symfony; + +use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; +use function array_key_exists; +use function is_string; + +trait HttpClientAssertionsTrait +{ + /** + * Asserts that the given URL has been called using, if specified, the given method body and headers. + * By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID. (It will succeed if the request has been called multiple times.) + */ + 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. + */ + 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. + */ + 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/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index f8bb9772..df2fd0f1 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,30 +14,47 @@ trait MailerAssertionsTrait { /** - * 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; otherwise this check will *always* pass. + * Asserts that the expected number of emails was sent. */ - 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). + * 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); + } + + /** + * 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 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); + } + + /** + * 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 after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. - * - * ```php - * <?php - * $I->seeEmailIsSent(2); - * ``` - * - * @param int $expectedCount The expected number of emails sent + * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; otherwise this check will *always* pass. */ - public function seeEmailIsSent(int $expectedCount = 1): void + public function dontSeeEmailIsSent(): void { - $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($expectedCount)); + $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount(0)); } /** @@ -78,6 +97,23 @@ 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. + * + * ```php + * <?php + * $I->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)); + } + protected function getMessageMailerEvents(): MessageEvents { if ($messageLogger = $this->getService('mailer.message_logger_listener')) { diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index d20ea306..a55b13ba 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -6,6 +6,7 @@ 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 @@ -133,6 +134,22 @@ public function assertEmailNotHasHeader(string $headerName, ?Email $email = null $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName))); } + /** + * 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 the text body of an email contains a `$text`. * If the Email object is not specified, the last email sent is used instead. diff --git a/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php b/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php new file mode 100644 index 00000000..c8b0f74c --- /dev/null +++ b/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace Codeception\Module\Symfony; + +use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\Notifier\Event\MessageEvent; +use Symfony\Component\Notifier\Event\NotificationEvents; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Test\Constraint\NotificationCount; +use Symfony\Component\Notifier\Test\Constraint\NotificationIsQueued; +use Symfony\Component\Notifier\Test\Constraint\NotificationSubjectContains; +use Symfony\Component\Notifier\Test\Constraint\NotificationTransportIsEqual; + +trait NotificationAssertionsTrait +{ + /** + * Asserts that the given number of notifications has been created (in total or for the given transport). + */ + public function assertNotificationCount(int $count, ?string $transportName = null, string $message = ''): void + { + $this->assertThat($this->getNotificationEvents(), new NotificationCount($count, $transportName), $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 notification is queued. + */ + public function assertNotificationIsQueued(MessageEvent $event, string $message = ''): void + { + $this->assertThat($event, 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); + } + + /** + * 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); + } + + 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?'); + } +} From 3f1a2b81da2e38356243204431d4d61d93baf2ed Mon Sep 17 00:00:00 2001 From: Dieter Beck <beck.worma@gmail.com> Date: Mon, 11 Nov 2024 08:12:47 +0100 Subject: [PATCH 27/34] Test against PHP 8.4 (#201) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index db3651cb..f7f897c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [8.1, 8.2, 8.3] + php: [8.1, 8.2, 8.3, 8.4] symfony: ["5.4.*", "6.4.*", "7.1.*"] exclude: - php: 8.1 From cad3c7a46da68f591dd363bc114ec9fde4f1dd63 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:45:04 -0500 Subject: [PATCH 28/34] Symfony assertion refinement (#200) --- composer.json | 2 +- readme.md | 2 +- src/Codeception/Module/Symfony.php | 2 - .../Module/Symfony/BrowserAssertionsTrait.php | 118 +++++++++--------- .../Symfony/DomCrawlerAssertionsTrait.php | 114 +++++------------ .../Module/Symfony/FormAssertionsTrait.php | 4 +- .../Module/Symfony/MimeAssertionsTrait.php | 16 --- .../Symfony/NotificationAssertionsTrait.php | 91 -------------- 8 files changed, 89 insertions(+), 260 deletions(-) delete mode 100644 src/Codeception/Module/Symfony/NotificationAssertionsTrait.php diff --git a/composer.json b/composer.json index 0bdc3bd6..65238b1d 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "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/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/readme.md b/readme.md index 06d2d614..ccedd850 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ A Codeception module for Symfony framework. ## Requirements -* `Symfony` `5.4.x`, `6.4.x`, `7.0.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). +* `Symfony` `5.4.x`, `6.4.x`, `7.1.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). * `PHP 8.1` or higher. ## Installation diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 34694898..3f7917bb 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -19,7 +19,6 @@ 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; @@ -144,7 +143,6 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule 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 cc8bfb54..488e1976 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -5,7 +5,6 @@ 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; @@ -25,18 +24,17 @@ trait BrowserAssertionsTrait { /** - * Asserts the given cookie in the test Client is set to the expected value. + * Asserts that 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); + $this->assertThatForClient(new BrowserHasCookie($name, $path, $domain), $message); + $this->assertThatForClient(new BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain), $message); } /** - * Asserts that the test Client does have the given cookie set (meaning, the cookie was set by any response in the test). + * Asserts that the test client has the specified cookie set. + * This indicates that the cookie was set by any response during the test. */ public function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -44,7 +42,8 @@ public function assertBrowserHasCookie(string $name, string $path = '/', ?string } /** - * Asserts that the test Client does not have the given cookie set (meaning, the cookie was set by any response in the test). + * Asserts that the test client does not have the specified cookie set. + * This indicates that the cookie was not set by any response during the test. */ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -52,7 +51,7 @@ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?str } /** - * Asserts the given request attribute is set to the expected value. + * Asserts that the specified request attribute matches the expected value. */ public function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void { @@ -60,18 +59,16 @@ public function assertRequestAttributeValueSame(string $name, string $expectedVa } /** - * Asserts the given cookie is present and set to the expected value. + * Asserts that the specified response cookie is present and matches 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); + $this->assertThatForResponse(new ResponseHasCookie($name, $path, $domain), $message); + $this->assertThatForResponse(new ResponseCookieValueSame($name, $expectedValue, $path, $domain), $message); } /** - * Asserts the response format returned by the `Response::getFormat()` method is the same as the expected value. + * Asserts that the response format matches the expected format. This checks the format returned by the `Response::getFormat()` method. */ public function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void { @@ -79,7 +76,7 @@ public function assertResponseFormatSame(?string $expectedFormat, string $messag } /** - * Asserts the given cookie is present in the response (optionally checking for a specific cookie path or domain). + * Asserts that the specified cookie is present in the response. Optionally, it can check for a specific cookie path or domain. */ public function assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -87,7 +84,8 @@ public function assertResponseHasCookie(string $name, string $path = '/', ?strin } /** - * Asserts the given header is available on the response, e.g. assertResponseHasHeader('content-type');. + * Asserts that the specified header is available in the response. + * For example, use `assertResponseHasHeader('content-type');`. */ public function assertResponseHasHeader(string $headerName, string $message = ''): void { @@ -95,8 +93,8 @@ public function assertResponseHasHeader(string $headerName, string $message = '' } /** - * Asserts the given header does not contain the expected value on the response, - * e.g. assertResponseHeaderNotSame('content-type', 'application/octet-stream');. + * Asserts that the specified header does not contain the expected value in the response. + * For example, use `assertResponseHeaderNotSame('content-type', 'application/octet-stream');`. */ public function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void { @@ -104,8 +102,8 @@ public function assertResponseHeaderNotSame(string $headerName, string $expected } /** - * Asserts the given header does contain the expected value on the response, - * e.g. assertResponseHeaderSame('content-type', 'application/octet-stream');. + * Asserts that the specified header contains the expected value in the response. + * For example, use `assertResponseHeaderSame('content-type', 'application/octet-stream');`. */ public function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void { @@ -113,7 +111,7 @@ public function assertResponseHeaderSame(string $headerName, string $expectedVal } /** - * Asserts that the response was successful (HTTP status is 2xx). + * Asserts that the response was successful (HTTP status code is in the 2xx range). */ public function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void { @@ -121,7 +119,7 @@ public function assertResponseIsSuccessful(string $message = '', bool $verbose = } /** - * Asserts the response is unprocessable (HTTP status is 422) + * Asserts that the response is unprocessable (HTTP status code is 422). */ public function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void { @@ -129,7 +127,7 @@ public function assertResponseIsUnprocessable(string $message = '', bool $verbos } /** - * Asserts the given cookie is not present in the response (optionally checking for a specific cookie path or domain). + * Asserts that the specified cookie is not present in the response. Optionally, it can check for a specific cookie path or domain. */ public function assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -137,7 +135,8 @@ public function assertResponseNotHasCookie(string $name, string $path = '/', ?st } /** - * Asserts the given header is not available on the response, e.g. assertResponseNotHasHeader('content-type');. + * Asserts that the specified header is not available in the response. + * For example, use `assertResponseNotHasHeader('content-type');`. */ public function assertResponseNotHasHeader(string $headerName, string $message = ''): void { @@ -145,30 +144,27 @@ public function assertResponseNotHasHeader(string $headerName, string $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. + * Asserts that the response is a redirect. Optionally, you can check the target location and status code. + * The expected 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); - } + $this->assertThatForResponse(new ResponseIsRedirected($verbose), $message); - $constraint = LogicalAnd::fromConstraints($constraint, $locationConstraint); + if ($expectedLocation) { + $constraint = class_exists(ResponseHeaderLocationSame::class) + ? new ResponseHeaderLocationSame($this->getClient()->getRequest(), $expectedLocation) + : new ResponseHeaderSame('Location', $expectedLocation); + $this->assertThatForResponse($constraint, $message); } + if ($expectedCode) { - $constraint = LogicalAnd::fromConstraints($constraint, new ResponseStatusCodeSame($expectedCode)); + $this->assertThatForResponse(new ResponseStatusCodeSame($expectedCode), $message); } - - $this->assertThatForResponse($constraint, $message); } /** - * Asserts a specific HTTP status code. + * Asserts that the response status code matches the expected code. */ public function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void { @@ -178,23 +174,18 @@ public function assertResponseStatusCodeSame(int $expectedCode, string $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 = []; + public function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void { + $request = $this->getClient()->getRequest(); + $this->assertThat($request, new RequestAttributeValueSame('_route', $expectedRoute)); + foreach ($parameters as $key => $value) { - $constraints[] = new RequestAttributeValueSame($key, $value); - } - if ($constraints) { - $constraint = LogicalAnd::fromConstraints($constraint, ...$constraints); + $this->assertThat($request, new RequestAttributeValueSame($key, $value), $message); } - - $this->assertThat($this->getClient()->getRequest(), $constraint, $message); } /** - * Reboot client's kernel. - * Can be used to manually reboot kernel when 'rebootable_client' => false + * Reboots the client's kernel. + * Can be used to manually reboot the kernel when 'rebootable_client' is set to false. * * ```php * <?php @@ -214,7 +205,7 @@ public function rebootClientKernel(): void /** * Verifies that a page is available. - * By default, it checks the current page, specify the `$url` parameter to change it. + * By default, it checks the current page. Specify the `$url` parameter to change the page being checked. * * ```php * <?php @@ -224,7 +215,7 @@ public function rebootClientKernel(): void * $I->seePageIsAvailable('/dashboard'); // Same as above * ``` * - * @param string|null $url + * @param string|null $url The URL of the page to check. If null, the current page is checked. */ public function seePageIsAvailable(?string $url = null): void { @@ -237,7 +228,7 @@ public function seePageIsAvailable(?string $url = null): void } /** - * Goes to a page and check that it redirects to another. + * Navigates to a page and verifies that it redirects to another page. * * ```php * <?php @@ -246,21 +237,24 @@ public function seePageIsAvailable(?string $url = null): void */ public function seePageRedirectsTo(string $page, string $redirectsTo): void { - $this->getClient()->followRedirects(false); + $client = $this->getClient(); + $client->followRedirects(false); $this->amOnPage($page); - $response = $this->getClient()->getResponse(); + $this->assertTrue( - $response->isRedirection() + $client->getResponse()->isRedirection(), + 'The response is not a redirection.' ); - $this->getClient()->followRedirect(); + + $client->followRedirect(); $this->seeInCurrentUrl($redirectsTo); } /** - * Submit a form specifying the form name only once. + * Submits a form by specifying the form name only once. * * Use this function instead of [`$I->submitForm()`](#submitForm) to avoid repeating the form name in the field selectors. - * If you customized the names of the field selectors use `$I->submitForm()` for full control. + * If you have customized the names of the field selectors, use `$I->submitForm()` for full control. * * ```php * <?php @@ -270,8 +264,8 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void * ]); * ``` * - * @param string $name The `name` attribute of the `<form>` (you cannot use an array as selector here) - * @param string[] $fields + * @param string $name The `name` attribute of the `<form>`. You cannot use an array as a selector here. + * @param array<string, mixed> $fields The form fields to submit. */ public function submitSymfonyForm(string $name, array $fields): void { diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index 0d5bca59..643e3fd1 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -4,62 +4,21 @@ namespace Codeception\Module\Symfony; -use PHPUnit\Framework\Constraint\LogicalAnd; +use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\LogicalNot; -use Symfony\Component\DomCrawler\Crawler; -use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextContains; -use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextSame; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorAttributeValueSame; -use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorCount; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorExists; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorTextContains; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorTextSame; trait DomCrawlerAssertionsTrait { - /** - * 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 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 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 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 - ); + $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked"), $message); } /** @@ -67,33 +26,32 @@ public function assertCheckboxChecked(string $fieldName, string $message = ''): */ public function assertCheckboxNotChecked(string $fieldName, string $message = ''): void { - $this->assertThat( - $this->getCrawler(), - new LogicalNot(new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked")), - $message - ); + $this->assertThatCrawler(new LogicalNot( + new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked") + ), $message); } /** - * Asserts that value of the form input with the given name does not equal the expected value. + * Asserts that the 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)) + $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message); + $this->assertThatCrawler(new LogicalNot( + new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) ), $message); } /** - * Asserts that value of the form input with the given name does equal the expected value. + * Asserts that the value of the form input with the given name equals 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); + $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message); + $this->assertThatCrawler( + new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue), + $message + ); } /** @@ -105,7 +63,7 @@ public function assertPageTitleContains(string $expectedTitle, string $message = } /** - * Asserts that the `<title>` element is equal to the given title. + * Asserts that the `<title>` element equals the given title. */ public function assertPageTitleSame(string $expectedTitle, string $message = ''): void { @@ -113,19 +71,11 @@ public function assertPageTitleSame(string $expectedTitle, string $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 the given selector does match at least one element in the response. + * Asserts that the given selector matches at least one element in the response. */ public function assertSelectorExists(string $selector, string $message = ''): void { - $this->assertThat($this->getCrawler(), new CrawlerSelectorExists($selector), $message); + $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message); } /** @@ -133,18 +83,16 @@ public function assertSelectorExists(string $selector, string $message = ''): vo */ public function assertSelectorNotExists(string $selector, string $message = ''): void { - $this->assertThat($this->getCrawler(), new LogicalNot(new CrawlerSelectorExists($selector)), $message); + $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorExists($selector)), $message); } /** - * Asserts that the first element matching the given selector does contain the expected text. + * Asserts that the first element matching the given selector contains 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); + $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message); + $this->assertThatCrawler(new CrawlerSelectorTextContains($selector, $text), $message); } /** @@ -152,25 +100,21 @@ public function assertSelectorTextContains(string $selector, string $text, strin */ 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); + $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message); + $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorTextContains($selector, $text)), $message); } /** - * Asserts that the contents of the first element matching the given selector does equal the expected text. + * Asserts that the text of the first element matching the given selector equals 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); + $this->assertThatCrawler(new CrawlerSelectorExists($selector), $message); + $this->assertThatCrawler(new CrawlerSelectorTextSame($selector, $text), $message); } - protected function getCrawler(): Crawler + protected function assertThatCrawler(Constraint $constraint, string $message): void { - return $this->client->getCrawler(); + $this->assertThat($this->getClient()->getCrawler(), $constraint, $message); } } diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 0c8736f0..cdebbb64 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -17,7 +17,7 @@ trait FormAssertionsTrait */ public function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void { - $node = $this->getCrawler()->filter($formSelector); + $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)); @@ -29,7 +29,7 @@ public function assertFormValue(string $formSelector, string $fieldName, string */ public function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void { - $node = $this->getCrawler()->filter($formSelector); + $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)); diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index a55b13ba..ba2ee9ac 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -134,22 +134,6 @@ public function assertEmailNotHasHeader(string $headerName, ?Email $email = null $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName))); } - /** - * 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 the text body of an email contains a `$text`. * If the Email object is not specified, the last email sent is used instead. diff --git a/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php b/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php deleted file mode 100644 index c8b0f74c..00000000 --- a/src/Codeception/Module/Symfony/NotificationAssertionsTrait.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Codeception\Module\Symfony; - -use PHPUnit\Framework\Constraint\LogicalNot; -use Symfony\Component\Notifier\Event\MessageEvent; -use Symfony\Component\Notifier\Event\NotificationEvents; -use Symfony\Component\Notifier\Message\MessageInterface; -use Symfony\Component\Notifier\Test\Constraint\NotificationCount; -use Symfony\Component\Notifier\Test\Constraint\NotificationIsQueued; -use Symfony\Component\Notifier\Test\Constraint\NotificationSubjectContains; -use Symfony\Component\Notifier\Test\Constraint\NotificationTransportIsEqual; - -trait NotificationAssertionsTrait -{ - /** - * Asserts that the given number of notifications has been created (in total or for the given transport). - */ - public function assertNotificationCount(int $count, ?string $transportName = null, string $message = ''): void - { - $this->assertThat($this->getNotificationEvents(), new NotificationCount($count, $transportName), $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 notification is queued. - */ - public function assertNotificationIsQueued(MessageEvent $event, string $message = ''): void - { - $this->assertThat($event, 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); - } - - /** - * 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); - } - - 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?'); - } -} From 5e042f1686bf7e4591eaa90a524f14cc796a9320 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:23:31 -0500 Subject: [PATCH 29/34] Support Symfony 7.2 (#203) --- .github/workflows/main.yml | 50 +++++++++++++---------------------- composer.json | 54 +++++++++++++++++++------------------- readme.md | 2 +- 3 files changed, 46 insertions(+), 60 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f897c8..85b0d49d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,18 +1,16 @@ name: CI - on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest - strategy: matrix: php: [8.1, 8.2, 8.3, 8.4] - symfony: ["5.4.*", "6.4.*", "7.1.*"] + symfony: ["5.4.*", "6.4.*", "7.2.*"] exclude: - php: 8.1 - symfony: "7.1.*" + symfony: "7.2.*" steps: - name: Checkout code @@ -26,40 +24,28 @@ jobs: extensions: ctype, iconv, intl, json, mbstring, pdo, pdo_sqlite coverage: none - - name: Checkout Symfony 5.4 Sample - if: "matrix.symfony == '5.4.*'" - uses: actions/checkout@v4 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "5.4" - - - name: Checkout Symfony 6.4 Sample - if: "matrix.symfony == '6.4.*'" - uses: actions/checkout@v4 - with: - repository: Codeception/symfony-module-tests - path: framework-tests - ref: "6.4" + - name: Set Symfony version reference + run: echo "SF_REF=${MATRIX_SYMFONY%.*}" >> $GITHUB_ENV + env: + MATRIX_SYMFONY: ${{ matrix.symfony }} - - name: Checkout Symfony 7.1 Sample - if: "matrix.symfony == '7.1.*'" + - name: Checkout Symfony ${{ env.SF_REF }} Sample uses: actions/checkout@v4 with: repository: Codeception/symfony-module-tests path: framework-tests - ref: "7.1" + ref: ${{ env.SF_REF }} - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache composer dependencies + - name: Cache Composer dependencies uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json', 'composer.lock') }} + restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer- - name: Install PHPUnit 10 run: composer require --dev --no-update "phpunit/phpunit=^10.0" @@ -78,27 +64,27 @@ jobs: composer require codeception/module-doctrine="3.*" --no-update composer update --prefer-dist --no-progress --no-dev - - name: Validate composer.json and composer.lock - run: composer validate + - name: Validate Composer files + run: composer validate --strict working-directory: framework-tests - - name: Install PHPUnit 10 in framework-tests + - name: Install PHPUnit in framework-tests run: composer require --dev --no-update "phpunit/phpunit=^10.0" working-directory: framework-tests - - name: Install Symfony Sample + - name: Prepare Symfony sample run: | composer remove codeception/codeception codeception/module-asserts codeception/module-doctrine codeception/lib-innerbrowser codeception/module-symfony --dev --no-update composer update --no-progress working-directory: framework-tests - - name: Prepare the test environment + - name: Setup Database run: | php bin/console doctrine:schema:update --force php bin/console doctrine:fixtures:load --quiet working-directory: framework-tests - - name: Run test suite + - name: Run tests run: | php vendor/bin/codecept build -c framework-tests php vendor/bin/codecept run Functional -c framework-tests diff --git a/composer.json b/composer.json index 65238b1d..82fb64f3 100644 --- a/composer.json +++ b/composer.json @@ -28,33 +28,33 @@ "codeception/module-asserts": "^3.0", "codeception/module-doctrine": "^3.1", "doctrine/orm": "^2.10", - "symfony/browser-kit": "^5.4 | ^6.4 | ^7.0", - "symfony/cache": "^5.4 | ^6.4 | ^7.0", - "symfony/config": "^5.4 | ^6.4 | ^7.0", - "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.0", - "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.0", - "symfony/dotenv": "^5.4 | ^6.4 | ^7.0", - "symfony/error-handler": "^5.4 | ^6.4 | ^7.0", - "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", - "symfony/routing": "^5.4 | ^6.4 | ^7.0", - "symfony/security-bundle": "^5.4 | ^6.4 | ^7.0", - "symfony/security-core": "^5.4 | ^6.4 | ^7.0", - "symfony/security-csrf": "^5.4 | ^6.4 | ^7.0", - "symfony/security-http": "^5.4 | ^6.4 | ^7.0", - "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.0", - "symfony/validator": "^5.4 | ^6.4 | ^7.0", - "symfony/var-exporter": "^5.4 | ^6.4 | ^7.0", + "symfony/browser-kit": "^5.4 | ^6.4 | ^7.2", + "symfony/cache": "^5.4 | ^6.4 | ^7.2", + "symfony/config": "^5.4 | ^6.4 | ^7.2", + "symfony/dependency-injection": "^5.4 | ^6.4 | ^7.2", + "symfony/dom-crawler": "^5.4 | ^6.4 | ^7.2", + "symfony/dotenv": "^5.4 | ^6.4 | ^7.2", + "symfony/error-handler": "^5.4 | ^6.4 | ^7.2", + "symfony/filesystem": "^5.4 | ^6.4 | ^7.2", + "symfony/form": "^5.4 | ^6.4 | ^7.2", + "symfony/framework-bundle": "^5.4 | ^6.4 | ^7.2", + "symfony/http-client": "^5.4 | ^6.4 | ^7.2", + "symfony/http-foundation": "^5.4 | ^6.4 | ^7.2", + "symfony/http-kernel": "^5.4 | ^6.4 | ^7.2", + "symfony/mailer": "^5.4 | ^6.4 | ^7.2", + "symfony/mime": "^5.4 | ^6.4 | ^7.2", + "symfony/notifier": "^5.4 | ^6.4 | ^7.2", + "symfony/options-resolver": "^5.4 | ^6.4 | ^7.2", + "symfony/property-access": "^5.4 | ^6.4 | ^7.2", + "symfony/property-info": "^5.4 | ^6.4 | ^7.2", + "symfony/routing": "^5.4 | ^6.4 | ^7.2", + "symfony/security-bundle": "^5.4 | ^6.4 | ^7.2", + "symfony/security-core": "^5.4 | ^6.4 | ^7.2", + "symfony/security-csrf": "^5.4 | ^6.4 | ^7.2", + "symfony/security-http": "^5.4 | ^6.4 | ^7.2", + "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.2", + "symfony/validator": "^5.4 | ^6.4 | ^7.2", + "symfony/var-exporter": "^5.4 | ^6.4 | ^7.2", "vlucas/phpdotenv": "^4.2 | ^5.4" }, "suggest": { diff --git a/readme.md b/readme.md index ccedd850..c5bbcb98 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ A Codeception module for Symfony framework. ## Requirements -* `Symfony` `5.4.x`, `6.4.x`, `7.1.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). +* `Symfony` `5.4.x`, `6.4.x`, `7.2.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). * `PHP 8.1` or higher. ## Installation From 94e411b3262d54b7ef3f411683271c49f50ad406 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 20 Jan 2025 06:01:44 -0500 Subject: [PATCH 30/34] Added Symfony Translation assertions (#205) --- composer.json | 1 + src/Codeception/Module/Symfony.php | 2 + .../Symfony/TranslationAssertionsTrait.php | 178 ++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 src/Codeception/Module/Symfony/TranslationAssertionsTrait.php diff --git a/composer.json b/composer.json index 82fb64f3..747a5941 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "symfony/security-core": "^5.4 | ^6.4 | ^7.2", "symfony/security-csrf": "^5.4 | ^6.4 | ^7.2", "symfony/security-http": "^5.4 | ^6.4 | ^7.2", + "symfony/translation": "^5.4 | ^6.4 | ^7.2", "symfony/twig-bundle": "^5.4 | ^6.4 | ^7.2", "symfony/validator": "^5.4 | ^6.4 | ^7.2", "symfony/var-exporter": "^5.4 | ^6.4 | ^7.2", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 3f7917bb..ebe6aee6 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -25,6 +25,7 @@ use Codeception\Module\Symfony\ServicesAssertionsTrait; use Codeception\Module\Symfony\SessionAssertionsTrait; use Codeception\Module\Symfony\TimeAssertionsTrait; +use Codeception\Module\Symfony\TranslationAssertionsTrait; use Codeception\Module\Symfony\TwigAssertionsTrait; use Codeception\Module\Symfony\ValidatorAssertionsTrait; use Codeception\TestInterface; @@ -148,6 +149,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use SecurityAssertionsTrait; use ServicesAssertionsTrait; use SessionAssertionsTrait; + use TranslationAssertionsTrait; use TimeAssertionsTrait; use TwigAssertionsTrait; use ValidatorAssertionsTrait; diff --git a/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php new file mode 100644 index 00000000..7c4c385a --- /dev/null +++ b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php @@ -0,0 +1,178 @@ +<?php + +declare(strict_types=1); + +namespace Codeception\Module\Symfony; + +use Symfony\Component\Translation\DataCollector\TranslationDataCollector; +use Symfony\Component\VarDumper\Cloner\Data; + +trait TranslationAssertionsTrait +{ + /** + * Asserts that no fallback translations were found. + * + * ```php + * <?php + * $I->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 + * <?php + * $I->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 + * <?php + * $count = $I->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 + * <?php + * $I->seeAllTranslationsDefined(); + * ``` + */ + public function seeAllTranslationsDefined(): void + { + $this->dontSeeMissingTranslations(); + $this->dontSeeFallbackTranslations(); + } + + /** + * Asserts that the default locale is the expected one. + * + * ```php + * <?php + * $I->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 + * <?php + * $I->seeFallbackLocalesAre(['es', 'fr']); + * ``` + * + * @param array $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 + * <?php + * $I->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 + * <?php + * $I->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); + } +} From 12fdf094fea572f8fd6e975c4f4b440d95684d44 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:35:08 -0500 Subject: [PATCH 31/34] Added Symfony Logger assertion (dontSeeDeprecations) (#206) --- src/Codeception/Module/Symfony.php | 2 + .../Module/Symfony/LoggerAssertionsTrait.php | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/Codeception/Module/Symfony/LoggerAssertionsTrait.php diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index ebe6aee6..0e41e787 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -17,6 +17,7 @@ use Codeception\Module\Symfony\EventsAssertionsTrait; use Codeception\Module\Symfony\FormAssertionsTrait; use Codeception\Module\Symfony\HttpClientAssertionsTrait; +use Codeception\Module\Symfony\LoggerAssertionsTrait; use Codeception\Module\Symfony\MailerAssertionsTrait; use Codeception\Module\Symfony\MimeAssertionsTrait; use Codeception\Module\Symfony\ParameterAssertionsTrait; @@ -142,6 +143,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use EventsAssertionsTrait; use FormAssertionsTrait; use HttpClientAssertionsTrait; + use LoggerAssertionsTrait; use MailerAssertionsTrait; use MimeAssertionsTrait; use ParameterAssertionsTrait; 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 @@ +<?php + +declare(strict_types=1); + +namespace Codeception\Module\Symfony; + +use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; +use Symfony\Component\VarDumper\Cloner\Data; +use function sprintf; + +trait LoggerAssertionsTrait +{ + /** + * Asserts that there are no deprecation messages in Symfony's log. + * + * ```php + * <?php + * $I->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); + } +} From e11bf68e2e720bccfc1d449bf86e4bc7155c0b2a Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:17:07 -0500 Subject: [PATCH 32/34] Document new Symfony assertions (#208) --- .../Module/Symfony/BrowserAssertionsTrait.php | 87 ++++++++++++++++++- .../Symfony/DomCrawlerAssertionsTrait.php | 55 ++++++++++++ .../Module/Symfony/FormAssertionsTrait.php | 17 +++- .../Symfony/HttpClientAssertionsTrait.php | 23 ++++- .../Module/Symfony/MailerAssertionsTrait.php | 59 +++++++++++-- 5 files changed, 226 insertions(+), 15 deletions(-) diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 488e1976..fbd8a075 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -25,6 +25,11 @@ trait BrowserAssertionsTrait { /** * Asserts that the given cookie in the test client is set to the expected value. + * + * ```php + * <?php + * $I->assertBrowserCookieValueSame('cookie_name', 'expected_value'); + * ``` */ public function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -35,6 +40,11 @@ public function assertBrowserCookieValueSame(string $name, string $expectedValue /** * Asserts that the test client has the specified cookie set. * This indicates that the cookie was set by any response during the test. + * + * ``` + * <?php + * $I->assertBrowserHasCookie('cookie_name'); + * ``` */ public function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -44,6 +54,11 @@ public function assertBrowserHasCookie(string $name, string $path = '/', ?string /** * Asserts that the test client does not have the specified cookie set. * This indicates that the cookie was not set by any response during the test. + * + * ```php + * <?php + * $I->assertBrowserNotHasCookie('cookie_name'); + * ``` */ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -52,6 +67,11 @@ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?str /** * Asserts that the specified request attribute matches the expected value. + * + * ```php + * <?php + * $I->assertRequestAttributeValueSame('attribute_name', 'expected_value'); + * ``` */ public function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void { @@ -60,6 +80,11 @@ public function assertRequestAttributeValueSame(string $name, string $expectedVa /** * Asserts that the specified response cookie is present and matches the expected value. + * + * ```php + * <?php + * $I->assertResponseCookieValueSame('cookie_name', 'expected_value'); + * ``` */ public function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -69,6 +94,11 @@ public function assertResponseCookieValueSame(string $name, string $expectedValu /** * Asserts that the response format matches the expected format. This checks the format returned by the `Response::getFormat()` method. + * + * ```php + * <?php + * $I->assertResponseFormatSame('json'); + * ``` */ public function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void { @@ -77,6 +107,11 @@ public function assertResponseFormatSame(?string $expectedFormat, string $messag /** * Asserts that the specified cookie is present in the response. Optionally, it can check for a specific cookie path or domain. + * + * ```php + * <?php + * $I->assertResponseHasCookie('cookie_name'); + * ``` */ public function assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -86,6 +121,11 @@ public function assertResponseHasCookie(string $name, string $path = '/', ?strin /** * Asserts that the specified header is available in the response. * For example, use `assertResponseHasHeader('content-type');`. + * + * ```php + * <?php + * $I->assertResponseHasHeader('content-type'); + * ``` */ public function assertResponseHasHeader(string $headerName, string $message = ''): void { @@ -95,6 +135,11 @@ public function assertResponseHasHeader(string $headerName, string $message = '' /** * Asserts that the specified header does not contain the expected value in the response. * For example, use `assertResponseHeaderNotSame('content-type', 'application/octet-stream');`. + * + * ```php + * <?php + * $I->assertResponseHeaderNotSame('content-type', 'application/json'); + * ``` */ public function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void { @@ -104,6 +149,11 @@ public function assertResponseHeaderNotSame(string $headerName, string $expected /** * Asserts that the specified header contains the expected value in the response. * For example, use `assertResponseHeaderSame('content-type', 'application/octet-stream');`. + * + * ```php + * <?php + * $I->assertResponseHeaderSame('content-type', 'application/json'); + * ``` */ public function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void { @@ -112,6 +162,11 @@ public function assertResponseHeaderSame(string $headerName, string $expectedVal /** * Asserts that the response was successful (HTTP status code is in the 2xx range). + * + * ```php + * <?php + * $I->assertResponseIsSuccessful(); + * ``` */ public function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void { @@ -120,6 +175,11 @@ public function assertResponseIsSuccessful(string $message = '', bool $verbose = /** * Asserts that the response is unprocessable (HTTP status code is 422). + * + * ```php + * <?php + * $I->assertResponseIsUnprocessable(); + * ``` */ public function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void { @@ -128,6 +188,11 @@ public function assertResponseIsUnprocessable(string $message = '', bool $verbos /** * Asserts that the specified cookie is not present in the response. Optionally, it can check for a specific cookie path or domain. + * + * ```php + * <?php + * $I->assertResponseNotHasCookie('cookie_name'); + * ``` */ public function assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void { @@ -136,7 +201,11 @@ public function assertResponseNotHasCookie(string $name, string $path = '/', ?st /** * Asserts that the specified header is not available in the response. - * For example, use `assertResponseNotHasHeader('content-type');`. + * + * ```php + * <?php + * $I->assertResponseNotHasHeader('content-type'); + * ``` */ public function assertResponseNotHasHeader(string $headerName, string $message = ''): void { @@ -146,6 +215,12 @@ public function assertResponseNotHasHeader(string $headerName, string $message = /** * Asserts that the response is a redirect. Optionally, you can check the target location and status code. * The expected location can be either an absolute or a relative path. + * + * ```php + * <?php + * // Check that '/admin' redirects to '/login' with status code 302 + * $I->assertResponseRedirects('/login', 302); + * ``` */ public function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true): void { @@ -165,6 +240,11 @@ public function assertResponseRedirects(?string $expectedLocation = null, ?int $ /** * Asserts that the response status code matches the expected code. + * + * ```php + * <?php + * $I->assertResponseStatusCodeSame(200); + * ``` */ public function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void { @@ -173,6 +253,11 @@ public function assertResponseStatusCodeSame(int $expectedCode, string $message /** * Asserts the request matches the given route and optionally route parameters. + * + * ```php + * <?php + * $I->assertRouteSame('profile', ['id' => 123]); + * ``` */ public function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void { $request = $this->getClient()->getRequest(); diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index 643e3fd1..8786be4c 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -15,6 +15,11 @@ trait DomCrawlerAssertionsTrait { /** * Asserts that the checkbox with the given name is checked. + * + * ```php + * <?php + * $I->assertCheckboxChecked('agree_terms'); + * ``` */ public function assertCheckboxChecked(string $fieldName, string $message = ''): void { @@ -23,6 +28,11 @@ public function assertCheckboxChecked(string $fieldName, string $message = ''): /** * Asserts that the checkbox with the given name is not checked. + * + * ```php + * <?php + * $I->assertCheckboxNotChecked('subscribe'); + * ``` */ public function assertCheckboxNotChecked(string $fieldName, string $message = ''): void { @@ -33,6 +43,11 @@ public function assertCheckboxNotChecked(string $fieldName, string $message = '' /** * Asserts that the value of the form input with the given name does not equal the expected value. + * + * ```php + * <?php + * $I->assertInputValueNotSame('username', 'admin'); + * ``` */ public function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void { @@ -44,6 +59,11 @@ public function assertInputValueNotSame(string $fieldName, string $expectedValue /** * Asserts that the value of the form input with the given name equals the expected value. + * + * ```php + * <?php + * $I->assertInputValueSame('username', 'johndoe'); + * ``` */ public function assertInputValueSame(string $fieldName, string $expectedValue, string $message = ''): void { @@ -56,6 +76,11 @@ public function assertInputValueSame(string $fieldName, string $expectedValue, s /** * Asserts that the `<title>` element contains the given title. + * + * ```php + * <?php + * $I->assertPageTitleContains('Welcome'); + * ``` */ public function assertPageTitleContains(string $expectedTitle, string $message = ''): void { @@ -64,6 +89,11 @@ public function assertPageTitleContains(string $expectedTitle, string $message = /** * Asserts that the `<title>` element equals the given title. + * + * ```php + * <?php + * $I->assertPageTitleSame('Home Page'); + * ``` */ public function assertPageTitleSame(string $expectedTitle, string $message = ''): void { @@ -72,6 +102,11 @@ public function assertPageTitleSame(string $expectedTitle, string $message = '') /** * Asserts that the given selector matches at least one element in the response. + * + * ```php + * <?php + * $I->assertSelectorExists('.main-content'); + * ``` */ public function assertSelectorExists(string $selector, string $message = ''): void { @@ -80,6 +115,11 @@ public function assertSelectorExists(string $selector, string $message = ''): vo /** * Asserts that the given selector does not match at least one element in the response. + * + * ```php + * <?php + * $I->assertSelectorNotExists('.error'); + * ``` */ public function assertSelectorNotExists(string $selector, string $message = ''): void { @@ -88,6 +128,11 @@ public function assertSelectorNotExists(string $selector, string $message = ''): /** * Asserts that the first element matching the given selector contains the expected text. + * + * ```php + * <?php + * $I->assertSelectorTextContains('h1', 'Dashboard'); + * ``` */ public function assertSelectorTextContains(string $selector, string $text, string $message = ''): void { @@ -97,6 +142,11 @@ public function assertSelectorTextContains(string $selector, string $text, strin /** * Asserts that the first element matching the given selector does not contain the expected text. + * + * ```php + * <?php + * $I->assertSelectorTextNotContains('p', 'error'); + * ``` */ public function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void { @@ -106,6 +156,11 @@ public function assertSelectorTextNotContains(string $selector, string $text, st /** * Asserts that the text of the first element matching the given selector equals the expected text. + * + * ```php + * <?php + * $I->assertSelectorTextSame('h1', 'Dashboard'); + * ``` */ public function assertSelectorTextSame(string $selector, string $text, string $message = ''): void { diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index cdebbb64..f77403bd 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -14,6 +14,11 @@ trait FormAssertionsTrait { /** * Asserts that value of the field of the first form matching the given selector does equal the expected value. + * + * ```php + * <?php + * $I->assertFormValue('#loginForm', 'username', 'john_doe'); + * ``` */ public function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void { @@ -25,7 +30,12 @@ public function assertFormValue(string $formSelector, string $fieldName, string } /** - * Asserts that value of the field of the first form matching the given selector does equal the expected value. + * Asserts that the field of the first form matching the given selector does not have a value. + * + * ```php + * <?php + * $I->assertNoFormValue('#registrationForm', 'middle_name'); + * ``` */ public function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void { @@ -128,7 +138,6 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi * If you want to specify the error messages, you can do so * by sending an associative array instead, with the key being * the name of the field and the error message the value. - * * This method will validate that the expected error message * is contained in the actual error message, that is, * you can specify either the entire error message or just a part of it: @@ -136,7 +145,7 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi * ```php * <?php * $I->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' * ]); * ``` @@ -191,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 index 9ac3a6e4..f6f322eb 100644 --- a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php @@ -12,7 +12,18 @@ trait HttpClientAssertionsTrait { /** * Asserts that the given URL has been called using, if specified, the given method body and headers. - * By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID. (It will succeed if the request has been called multiple times.) + * By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID. + * (It will succeed if the request has been called multiple times.) + * + * ```php + * <?php + * $I->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 { @@ -77,6 +88,11 @@ public function assertHttpClientRequest(string $expectedUrl, string $expectedMet /** * 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 + * <?php + * $I->assertHttpClientRequestCount(3); + * ``` */ public function assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client'): void { @@ -88,6 +104,11 @@ public function assertHttpClientRequestCount(int $count, string $httpClientId = /** * 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 + * <?php + * $I->assertNotHttpClientRequest('https://example.com/unexpected', 'GET'); + * ``` */ public function assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client'): void { diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index df2fd0f1..007e02af 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -15,6 +15,11 @@ trait MailerAssertionsTrait { /** * Asserts that the expected number of emails was sent. + * + * ```php + * <?php + * $I->assertEmailCount(2, 'smtp'); + * ``` */ public function assertEmailCount(int $count, ?string $transport = null, string $message = ''): void { @@ -24,6 +29,12 @@ public function assertEmailCount(int $count, ?string $transport = null, string $ /** * 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 + * <?php + * $event = $I->getMailerEvent(); + * $I->assertEmailIsNotQueued($event); + * ``` */ public function assertEmailIsNotQueued(MessageEvent $event, string $message = ''): void { @@ -33,6 +44,12 @@ public function assertEmailIsNotQueued(MessageEvent $event, string $message = '' /** * Asserts that the given mailer event is queued. * Use `getMailerEvent(int $index = 0, ?string $transport = null)` to retrieve a mailer event by index. + * + * ```php + * <?php + * $event = $I->getMailerEvent(); + * $I->assertEmailIsQueued($event); + * ``` */ public function assertEmailIsQueued(MessageEvent $event, string $message = ''): void { @@ -41,6 +58,11 @@ public function assertEmailIsQueued(MessageEvent $event, string $message = ''): /** * Asserts that the expected number of emails was queued (e.g. using the Messenger component). + * + * ```php + * <?php + * $I->assertQueuedEmailCount(1, 'smtp'); + * ``` */ public function assertQueuedEmailCount(int $count, ?string $transport = null, string $message = ''): void { @@ -50,7 +72,13 @@ public function assertQueuedEmailCount(int $count, ?string $transport = null, st /** * 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; otherwise this check will *always* pass. + * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; + * otherwise this check will *always* pass. + * + * ```php + * <?php + * $I->dontSeeEmailIsSent(); + * ``` */ public function dontSeeEmailIsSent(): void { @@ -114,18 +142,31 @@ public function seeEmailIsSent(int $expectedCount = 1): void $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($expectedCount)); } + /** + * Returns the mailer event at the specified index. + * + * ```php + * <?php + * $event = $I->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("Emails can't be tested without Symfony Mailer service."); } } From 15fd14260b5041ec245bb9ffc146f4887f379e43 Mon Sep 17 00:00:00 2001 From: prophetz <kpuzuc@gmail.com> Date: Wed, 9 Apr 2025 23:48:38 +0300 Subject: [PATCH 33/34] Fix parameter name in exception for case when Kernel has custom namespace (#199) --- src/Codeception/Module/Symfony.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 0e41e787..3ac2bc79 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -321,7 +321,7 @@ protected function getKernelClass(): string throw new ModuleRequireException( self::class, "Kernel class was not found.\n" - . 'Specify directory where file with Kernel class for your application is located with `app_path` parameter.' + . 'Specify directory where file with Kernel class for your application is located with `kernel_class` parameter.' ); } From d9084f562015e5749a66647b977be0710cdc65d4 Mon Sep 17 00:00:00 2001 From: Thomas Landauer <thomas@landauer.at> Date: Thu, 10 Apr 2025 00:03:00 +0200 Subject: [PATCH 34/34] Update MailerAssertionsTrait.php: Adding Mailpit (#204) --- .../Module/Symfony/MailerAssertionsTrait.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 007e02af..5a31e6d8 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -72,8 +72,7 @@ public function assertQueuedEmailCount(int $count, ?string $transport = null, st /** * 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first; - * otherwise this check will *always* pass. + * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first; otherwise this check will *always* pass. * * ```php * <?php @@ -88,7 +87,7 @@ public function dontSeeEmailIsSent(): void /** * Returns the last sent email. * The function 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. + * 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 @@ -110,7 +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 an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. + * 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 @@ -128,7 +127,12 @@ public function grabSentEmails(): array /** * 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()](https://codeception.com/docs/modules/Symfony#stopFollowingRedirects) first. + * 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 * <?php