From a516dc6fc04db11d9d65f8de36297babf8fc16de Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:27:12 -0500 Subject: [PATCH 1/3] Standardize to PHP 8.2 and PSR-4 (#217) --- .github/workflows/main.yml | 16 +++++++++++++--- .gitignore | 3 ++- composer.json | 14 +++++++++++--- readme.md | 2 +- src/Codeception/Module/Symfony.php | 9 +++++++-- .../Module/Symfony/BrowserAssertionsTrait.php | 6 ++---- .../Module/Symfony/ConsoleAssertionsTrait.php | 14 +++++++------- .../Module/Symfony/DomCrawlerAssertionsTrait.php | 6 ++++-- .../Module/Symfony/EventsAssertionsTrait.php | 1 + .../Module/Symfony/FormAssertionsTrait.php | 1 + .../Module/Symfony/HttpClientAssertionsTrait.php | 7 ++++--- .../Module/Symfony/LoggerAssertionsTrait.php | 3 ++- .../Module/Symfony/SecurityAssertionsTrait.php | 1 + .../Module/Symfony/SessionAssertionsTrait.php | 1 + .../Module/Symfony/TimeAssertionsTrait.php | 1 + .../Module/Symfony/TwigAssertionsTrait.php | 1 + .../Module/Symfony/ValidatorAssertionsTrait.php | 2 +- 17 files changed, 60 insertions(+), 28 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ddd65355..8027f64b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,8 @@ jobs: matrix: php: [8.2, 8.3, 8.4] symfony: ["5.4.*", "6.4.*", "6.4wApi", "7.3.*"] + env: + only_sf_latest: &only_sf_latest ${{ matrix.symfony == '7.3.*' }} steps: - name: Checkout code @@ -17,7 +19,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - tools: composer:v2, phpstan + tools: composer:v2 extensions: ctype, iconv, intl, json, mbstring, pdo, pdo_sqlite coverage: none @@ -79,8 +81,16 @@ jobs: composer update --prefer-dist --no-progress - name: Run PHPStan (max) - if: ${{ matrix.symfony == '7.2.*' }} - run: phpstan analyse src --level=max --no-progress --error-format=github --memory-limit=1G + if: *only_sf_latest + run: composer phpstan + + - name: Run PHP-CS-Fixer + if: *only_sf_latest + run: composer cs-check + + - name: Run Composer Audit + if: *only_sf_latest + run: composer audit - name: Validate Composer files run: composer validate --strict diff --git a/.gitignore b/.gitignore index 55018224..a8169301 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ /vendor/ /composer.lock -/framework-tests \ No newline at end of file +/framework-tests +/.php-cs-fixer.cache \ No newline at end of file diff --git a/composer.json b/composer.json index b2202e83..03ca4985 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,8 @@ "codeception/module-asserts": "^3.0", "codeception/module-doctrine": "^3.1", "doctrine/orm": "^3.5", + "friendsofphp/php-cs-fixer": "^3.85", + "phpstan/phpstan": "^2.1", "symfony/browser-kit": "^5.4 | ^6.4 | ^7.3", "symfony/cache": "^5.4 | ^6.4 | ^7.3", "symfony/config": "^5.4 | ^6.4 | ^7.3", @@ -66,11 +68,17 @@ "symfony/web-profiler-bundle": "Tool that gives information about the execution of requests" }, "autoload": { - "classmap": ["src/"] + "psr-4": { + "Codeception\\": "src/Codeception/" + } }, "config": { - "classmap-authoritative": true, "sort-packages": true }, - "minimum-stability": "RC" + "minimum-stability": "RC", + "scripts": { + "phpstan": "phpstan analyse src --level=max --memory-limit=1G", + "cs-check": "php-cs-fixer fix src --dry-run --diff --using-cache=no", + "cs-fix": "php-cs-fixer fix src --using-cache=no" + } } diff --git a/readme.md b/readme.md index 79a33595..34cac759 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ A Codeception module for Symfony framework. ## Requirements * `Symfony` `5.4.x`, `6.4.x`, `7.3.x` or higher, as per the [Symfony supported versions](https://symfony.com/releases). -* `PHP 8.1` or higher. +* `PHP 8.2` or higher. ## Installation diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index c3f93dac..0f98cf13 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -54,6 +54,7 @@ use Symfony\Component\Mailer\DataCollector\MessageDataCollector; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\VarDumper\Cloner\Data; + use function array_keys; use function array_map; use function array_unique; @@ -64,6 +65,7 @@ use function file_exists; use function implode; use function in_array; +use function extension_loaded; use function ini_get; use function ini_set; use function is_object; @@ -292,8 +294,7 @@ public function _getEntityManager(): EntityManagerInterface $this->persistPermanentService($emService); $container = $this->_getContainer(); foreach ( - ['doctrine', 'doctrine.orm.default_entity_manager', 'doctrine.dbal.default_connection'] - as $service + ['doctrine', 'doctrine.orm.default_entity_manager', 'doctrine.dbal.default_connection'] as $service ) { if ($container->has($service)) { $this->persistPermanentService($service); @@ -495,6 +496,10 @@ protected function getInternalDomains(): array */ private function setXdebugMaxNestingLevel(int $max): void { + if (!extension_loaded('xdebug')) { + return; + } + if ((int) ini_get('xdebug.max_nesting_level') < $max) { ini_set('xdebug.max_nesting_level', (string) $max); } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 760f6cc7..8bd940b7 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -19,6 +19,7 @@ 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 @@ -329,10 +330,7 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void $client->followRedirects(false); $this->amOnPage($page); - $this->assertTrue( - $client->getResponse()->isRedirection(), - 'The response is not a redirection.' - ); + $this->assertThatForResponse(new ResponseIsRedirected(), 'The response is not a redirection.'); $client->followRedirect(); $this->seeInCurrentUrl($redirectsTo); diff --git a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php index 7b9dab27..5239bca1 100644 --- a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php @@ -78,19 +78,19 @@ private function configureOptions(array $parameters): array $options['interactive'] = false; } - if (in_array('-vvv', $parameters, true) - || in_array('--verbose=3', $parameters, true) + 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) + } 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) + } 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; diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index f25f9bf3..ec313bd6 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -39,7 +39,8 @@ public function assertCheckboxNotChecked(string $fieldName, string $message = '' $this->assertThatCrawler( new LogicalNot( new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked") - ), $message + ), + $message ); } @@ -57,7 +58,8 @@ public function assertInputValueNotSame(string $fieldName, string $expectedValue $this->assertThatCrawler( new LogicalNot( new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) - ), $message + ), + $message ); } diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index d9ba09f0..3108351e 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Assert; use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; + use function array_column; use function array_merge; use function count; diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 26ab54aa..09314c6e 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -6,6 +6,7 @@ use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; use Symfony\Component\VarDumper\Cloner\Data; + use function is_array; use function is_int; use function is_string; diff --git a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php index a8124b5f..bc55ae8f 100644 --- a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php @@ -6,6 +6,7 @@ use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; use Symfony\Component\VarDumper\Cloner\Data; + use function array_change_key_case; use function array_filter; use function array_intersect_key; @@ -56,10 +57,10 @@ function (array $trace) use ($expectedUrl, $expectedMethod, $expectedBody, $expe $bodyMatches = $expectedBody === null || $expectedBody === $actualBody; $headersMatch = $expectedHeaders === [] || ( - is_array($headerValues = $this->extractValue($options['headers'] ?? [])) + is_array($headerValues = $this->extractValue($options['headers'] ?? [])) && ($normalizedExpected = array_change_key_case($expectedHeaders)) === array_intersect_key(array_change_key_case($headerValues), $normalizedExpected) - ); + ); return $bodyMatches && $headersMatch; }, @@ -100,7 +101,7 @@ public function assertNotHttpClientRequest( ): void { $matchingRequests = array_filter( $this->getHttpClientTraces($httpClientId, __FUNCTION__), - fn(array $trace): bool => $this->matchesUrlAndMethod($trace, $unexpectedUrl, $unexpectedMethod) + fn (array $trace): bool => $this->matchesUrlAndMethod($trace, $unexpectedUrl, $unexpectedMethod) ); $this->assertEmpty( diff --git a/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php b/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php index 149b1a08..f7fdbbcc 100644 --- a/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/LoggerAssertionsTrait.php @@ -6,6 +6,7 @@ use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Component\VarDumper\Cloner\Data; + use function sprintf; trait LoggerAssertionsTrait @@ -45,7 +46,7 @@ public function dontSeeDeprecations(string $message = ''): void "Found %d deprecation message%s in the log:\n%s", $count, $count !== 1 ? 's' : '', - implode("\n", array_map(static fn(string $m): string => " - $m", $foundDeprecations)), + implode("\n", array_map(static fn (string $m): string => " - $m", $foundDeprecations)), ); $this->assertEmpty($foundDeprecations, $errorMessage); } diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index 3d0ecaf6..38025985 100644 --- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -10,6 +10,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; + use function sprintf; trait SecurityAssertionsTrait diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index 9382184a..70528946 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + use function class_exists; use function in_array; use function is_int; diff --git a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php index 1affaba6..15f89115 100644 --- a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php @@ -5,6 +5,7 @@ namespace Codeception\Module\Symfony; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; + use function round; use function sprintf; diff --git a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php index 61899979..021e6456 100644 --- a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php @@ -5,6 +5,7 @@ namespace Codeception\Module\Symfony; use Symfony\Bridge\Twig\DataCollector\TwigDataCollector; + use function array_key_first; trait TwigAssertionsTrait diff --git a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php index 01875a04..bfbdac07 100644 --- a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php @@ -91,7 +91,7 @@ protected function getViolationsForSubject(object $subject, ?string $propertyPat if ($constraint !== null) { return (array)array_filter( $violations, - static fn(ConstraintViolationInterface $violation): bool => get_class((object)$violation->getConstraint()) === $constraint && + static fn (ConstraintViolationInterface $violation): bool => get_class((object)$violation->getConstraint()) === $constraint && ($propertyPath === null || $violation->getPropertyPath() === $propertyPath) ); } From f1b6bf150495cccad802352fde85c22a321c8c4e Mon Sep 17 00:00:00 2001 From: dmitrii <88384601+d-mitrofanov-v@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:32:26 +0200 Subject: [PATCH 2/3] Add notifier assertions trait (#220) * add notifier assertions trait * add check for symfony version for notifier assertions * fix missing parenthesis * add return types and ignore if.alwaysFalse on symfony version check --- src/Codeception/Module/Symfony.php | 19 +- .../Module/Symfony/DataCollectorName.php | 1 + .../Symfony/NotifierAssertionsTrait.php | 264 ++++++++++++++++++ 3 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/Codeception/Module/Symfony/NotifierAssertionsTrait.php diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 0f98cf13..ff11a36b 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -21,6 +21,7 @@ use Codeception\Module\Symfony\LoggerAssertionsTrait; use Codeception\Module\Symfony\MailerAssertionsTrait; use Codeception\Module\Symfony\MimeAssertionsTrait; +use Codeception\Module\Symfony\NotifierAssertionsTrait; use Codeception\Module\Symfony\ParameterAssertionsTrait; use Codeception\Module\Symfony\RouterAssertionsTrait; use Codeception\Module\Symfony\SecurityAssertionsTrait; @@ -52,6 +53,7 @@ use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Mailer\DataCollector\MessageDataCollector; +use Symfony\Component\Notifier\DataCollector\NotificationDataCollector; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\VarDumper\Cloner\Data; @@ -156,6 +158,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use LoggerAssertionsTrait; use MailerAssertionsTrait; use MimeAssertionsTrait; + use NotifierAssertionsTrait; use ParameterAssertionsTrait; use RouterAssertionsTrait; use SecurityAssertionsTrait; @@ -412,8 +415,9 @@ protected function getProfile(): ?Profile * ($collector is DataCollectorName::TWIG ? TwigDataCollector : * ($collector is DataCollectorName::SECURITY ? SecurityDataCollector : * ($collector is DataCollectorName::MAILER ? MessageDataCollector : + * ($collector is DataCollectorName::NOTIFIER ? NotificationDataCollector : * DataCollectorInterface - * )))))))) + * ))))))))) * ) * * @throws AssertionFailedError @@ -465,6 +469,13 @@ protected function debugResponse(mixed $url): void } } + if ($profile->hasCollector(DataCollectorName::NOTIFIER->value)) { + $notifierCollector = $profile->getCollector(DataCollectorName::NOTIFIER->value); + if ($notifierCollector instanceof NotificationDataCollector) { + $this->debugNotifierData($notifierCollector); + } + } + if ($profile->hasCollector(DataCollectorName::TIME->value)) { $timeCollector = $profile->getCollector(DataCollectorName::TIME->value); if ($timeCollector instanceof TimeDataCollector) { @@ -543,6 +554,12 @@ private function debugMailerData(MessageDataCollector $messageCollector): void $this->debugSection('Emails', sprintf('%d sent', $count)); } + private function debugNotifierData(NotificationDataCollector $notificationCollector): void + { + $count = count($notificationCollector->getEvents()->getMessages()); + $this->debugSection('Notifications', sprintf('%d sent', $count)); + } + private function debugTimeData(TimeDataCollector $timeCollector): void { $this->debugSection('Time', sprintf('%.2f ms', $timeCollector->getDuration())); diff --git a/src/Codeception/Module/Symfony/DataCollectorName.php b/src/Codeception/Module/Symfony/DataCollectorName.php index efa86872..e6eb392a 100644 --- a/src/Codeception/Module/Symfony/DataCollectorName.php +++ b/src/Codeception/Module/Symfony/DataCollectorName.php @@ -18,4 +18,5 @@ enum DataCollectorName: string case TWIG = 'twig'; case SECURITY = 'security'; case MAILER = 'mailer'; + case NOTIFIER = 'notifier'; } diff --git a/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php new file mode 100644 index 00000000..77422c72 --- /dev/null +++ b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php @@ -0,0 +1,264 @@ +assertNotificationCount(2, 'smtp'); + * ``` + */ + public function assertNotificationCount(int $count, ?string $transportName = null, string $message = ''): void + { + $this->assertThat($this->getNotificationEvents(), new NotifierConstraint\NotificationCount($count, $transportName), $message); + } + + /** + * Asserts that the given notifier event is not queued. + * Use `getNotifierEvent(int $index = 0, ?string $transportName = null)` to retrieve a notifier event by index. + * + * ```php + * getNotifierEvent(); + * $I->asserNotificationIsNotQueued($event); + * ``` + */ + public function assertNotificationIsNotQueued(MessageEvent $event, string $message = ''): void + { + $this->assertThat($event, new LogicalNot(new NotifierConstraint\NotificationIsQueued()), $message); + } + + /** + * Asserts that the given notifier event is queued. + * Use `getNotifierEvent(int $index = 0, ?string $transportName = null)` to retrieve a notifier event by index. + * + * ```php + * getNotifierEvent(); + * $I->assertNotificationlIsQueued($event); + * ``` + */ + public function assertNotificationIsQueued(MessageEvent $event, string $message = ''): void + { + $this->assertThat($event, new NotifierConstraint\NotificationIsQueued(), $message); + } + + /** + * Asserts that the given notification contains given subject. + * Use `getNotifierMessage(int $index = 0, ?string $transportName = null)` to retrieve a notification by index. + * + * ```php + * getNotifierMessage(); + * $I->assertNotificationSubjectContains($notification, 'Subject'); + * ``` + */ + public function assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = ''): void + { + $this->assertThat($notification, new NotifierConstraint\NotificationSubjectContains($text), $message); + } + + /** + * Asserts that the given notification does not contain given subject. + * Use `getNotifierMessage(int $index = 0, ?string $transportName = null)` to retrieve a notification by index. + * + * ```php + * getNotifierMessage(); + * $I->assertNotificationSubjectNotContains($notification, 'Subject'); + * ``` + */ + public function assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = ''): void + { + $this->assertThat($notification, new LogicalNot(new NotifierConstraint\NotificationSubjectContains($text)), $message); + } + + /** + * Asserts that the given notification uses given transport. + * Use `getNotifierMessage(int $index = 0, ?string $transportName = null)` to retrieve a notification by index. + * + * ```php + * getNotifierMessage(); + * $I->assertNotificationTransportIsEqual($notification, 'chat'); + * ``` + */ + public function assertNotificationTransportIsEqual(MessageInterface $notification, ?string $transportName = null, string $message = ''): void + { + $this->assertThat($notification, new NotifierConstraint\NotificationTransportIsEqual($transportName), $message); + } + + /** + * Asserts that the given notification does not use given transport. + * Use `getNotifierMessage(int $index = 0, ?string $transportName = null)` to retrieve a notification by index. + * + * ```php + * getNotifierMessage(); + * $I->assertNotificationTransportIsNotEqual($notification, 'transport'); + * ``` + */ + public function assertNotificationTransportIsNotEqual(MessageInterface $notification, ?string $transportName = null, string $message = ''): void + { + $this->assertThat($notification, new LogicalNot(new NotifierConstraint\NotificationTransportIsEqual($transportName)), $message); + } + + /** + * Asserts that the expected number of notifications was queued (e.g. using the Notifier component). + * + * ```php + * assertQueuedNotificationCount(1, 'smtp'); + * ``` + */ + public function assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = ''): void + { + $this->assertThat($this->getNotificationEvents(), new NotifierConstraint\NotificationCount($count, $transportName, true), $message); + } + + /** + * Checks that no notification was sent. + * The check is based on `\Symfony\Component\Notifier\EventListener\NotificationLoggerListener`, which means: + * If your app performs an HTTP redirect, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first; otherwise this check will *always* pass. + * + * ```php + * dontSeeNotificationIsSent(); + * ``` + */ + public function dontSeeNotificationIsSent(): void + { + $this->assertThat($this->getNotificationEvents(), new NotifierConstraint\NotificationCount(0)); + } + + /** + * Returns the last sent notification. + * The check is based on `\Symfony\Component\Notifier\EventListener\NotificationLoggerListener`, which means: + * If your app performs an HTTP redirect after sending the notification, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first. + * See also: [grabSentNotifications()](https://codeception.com/docs/modules/Symfony#grabSentNotifications) + * + * ```php + * grabLastSentNotification(); + * $I->assertSame('Subject', $message->getSubject()); + * ``` + */ + public function grabLastSentNotification(): ?MessageInterface + { + $notification = $this->getNotifierMessages(); + $lastNotification = end($notification); + + return $lastNotification ?: null; + } + + + /** + * Returns an array of all sent notifications. + * The check is based on `\Symfony\Component\Notifier\EventListener\NotificationLoggerListener`, which means: + * If your app performs an HTTP redirect after sending the notification, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first. + * See also: [grabLastSentNotification()](https://codeception.com/docs/modules/Symfony#grabLastSentNotification) + * + * ```php + * grabSentNotifications(); + * ``` + * + * @return MessageInterface[] + */ + public function grabSentNotifications(): array + { + return $this->getNotifierMessages(); + } + + /** + * Checks if the given number of notifications was sent (default `$expectedCount`: 1). + * The check is based on `\Symfony\Component\Notifier\EventListener\NotificationLoggerListener`, which means: + * If your app performs an HTTP redirect after sending the notification, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first. + * + * ```php + * seeNotificatoinIsSent(2); + * ``` + * + * @param int $expectedCount The expected number of notifications sent + */ + public function seeNotificationIsSent(int $expectedCount = 1): void + { + $this->assertThat($this->getNotificationEvents(), new NotifierConstraint\NotificationCount($expectedCount)); + } + + /** + * @return MessageEvent[] + */ + public function getNotifierEvents(?string $transportName = null): array + { + return $this->getNotificationEvents()->getEvents($transportName); + } + + /** + * Returns the notifier event at the specified index. + * + * ```php + * getNotifierEvent(); + * ``` + */ + public function getNotifierEvent(int $index = 0, ?string $transportName = null): ?MessageEvent + { + return $this->getNotifierEvents($transportName)[$index] ?? null; + } + + /** + * @return MessageInterface[] + */ + public function getNotifierMessages(?string $transportName = null): array + { + return $this->getNotificationEvents()->getMessages($transportName); + } + + /** + * Returns the notifier message at the specified index. + * + * ```php + * getNotifierMessage(); + * ``` + */ + public function getNotifierMessage(int $index = 0, ?string $transportName = null): ?MessageInterface + { + return $this->getNotifierMessages($transportName)[$index] ?? null; + } + + protected function getNotificationEvents(): NotificationEvents + { + // @phpstan-ignore if.alwaysFalse + if (version_compare(Kernel::VERSION, '6.2', '<')) { + Assert::fail('Notifier assertions require Symfony 6.2 or higher.'); + } + + $notifier = $this->getService('notifier.notification_logger_listener'); + if ($notifier instanceof NotificationLoggerListener) { + return $notifier->getEvents(); + } + $notifier = $this->getService('notifier.logger_notification_listener'); + if ($notifier instanceof NotificationLoggerListener) { + return $notifier->getEvents(); + } + Assert::fail("Notifications can't be tested without Symfony Notifier service."); + } +} From 70e5e06b0de5a4e7520e303ea6bac096e1f62ea9 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 23 Nov 2025 10:11:04 -0500 Subject: [PATCH 3/3] Simplify the module code (#222) --- composer.json | 1 + src/Codeception/Module/Symfony.php | 79 +++++++------------ .../Module/Symfony/BrowserAssertionsTrait.php | 3 +- .../Module/Symfony/MailerAssertionsTrait.php | 13 ++- .../Symfony/NotifierAssertionsTrait.php | 13 ++- 5 files changed, 42 insertions(+), 67 deletions(-) diff --git a/composer.json b/composer.json index 03ca4985..810b0b3a 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "doctrine/orm": "^3.5", "friendsofphp/php-cs-fixer": "^3.85", "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0", "symfony/browser-kit": "^5.4 | ^6.4 | ^7.3", "symfony/cache": "^5.4 | ^6.4 | ^7.3", "symfony/config": "^5.4 | ^6.4 | ^7.3", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index ff11a36b..2c87f529 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -334,6 +334,14 @@ protected function getClient(): SymfonyConnector */ protected function getKernelClass(): string { + /** @var class-string $kernelClass */ + $kernelClass = $this->config['kernel_class']; + $this->requireAdditionalAutoloader(); + + if (class_exists($kernelClass)) { + return $kernelClass; + } + /** @var string $rootDir */ $rootDir = codecept_root_dir(); $path = $rootDir . $this->config['app_path']; @@ -346,40 +354,21 @@ protected function getKernelClass(): string ); } - $this->requireAdditionalAutoloader(); - - $finder = new Finder(); - $results = iterator_to_array($finder->name('*Kernel.php')->depth('0')->in($path)); - - if ($results === []) { - throw new ModuleRequireException( - self::class, - "File with Kernel class was not found at {$path}.\n" . - 'Specify directory where file with Kernel class for your application is located with `app_path` parameter.' - ); - } - - $kernelClass = $this->config['kernel_class']; - $filesRealPath = []; + $finder = new Finder(); + $finder->name('*Kernel.php')->depth('0')->in($path); - foreach ($results as $file) { + foreach ($finder as $file) { include_once $file->getRealPath(); - $filesRealPath[] = $file->getRealPath(); } - if (class_exists($kernelClass)) { - $ref = new ReflectionClass($kernelClass); - $fileName = $ref->getFileName(); - if ($fileName !== false && in_array($fileName, $filesRealPath, true)) { - /** @var class-string $kernelClass */ - return $kernelClass; - } + if (class_exists($kernelClass, false)) { + return $kernelClass; } throw new ModuleRequireException( self::class, - "Kernel class was not found.\n" . - 'Specify directory where file with Kernel class for your application is located with `kernel_class` parameter.' + "Kernel class was not found at {$path}.\n" . + 'Specify directory where file with Kernel class for your application is located with `app_path` parameter.' ); } @@ -455,31 +444,19 @@ protected function debugResponse(mixed $url): void return; } - if ($profile->hasCollector(DataCollectorName::SECURITY->value)) { - $securityCollector = $profile->getCollector(DataCollectorName::SECURITY->value); - if ($securityCollector instanceof SecurityDataCollector) { - $this->debugSecurityData($securityCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::MAILER->value)) { - $mailerCollector = $profile->getCollector(DataCollectorName::MAILER->value); - if ($mailerCollector instanceof MessageDataCollector) { - $this->debugMailerData($mailerCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::NOTIFIER->value)) { - $notifierCollector = $profile->getCollector(DataCollectorName::NOTIFIER->value); - if ($notifierCollector instanceof NotificationDataCollector) { - $this->debugNotifierData($notifierCollector); - } - } - - if ($profile->hasCollector(DataCollectorName::TIME->value)) { - $timeCollector = $profile->getCollector(DataCollectorName::TIME->value); - if ($timeCollector instanceof TimeDataCollector) { - $this->debugTimeData($timeCollector); + $collectors = [ + DataCollectorName::SECURITY->value => [$this->debugSecurityData(...), SecurityDataCollector::class], + DataCollectorName::MAILER->value => [$this->debugMailerData(...), MessageDataCollector::class], + DataCollectorName::NOTIFIER->value => [$this->debugNotifierData(...), NotificationDataCollector::class], + DataCollectorName::TIME->value => [$this->debugTimeData(...), TimeDataCollector::class], + ]; + + foreach ($collectors as $name => [$callback, $expectedClass]) { + if ($profile->hasCollector($name)) { + $collector = $profile->getCollector($name); + if ($collector instanceof $expectedClass) { + $callback($collector); + } } } } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 8bd940b7..d04b69e1 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -359,8 +359,7 @@ public function submitSymfonyForm(string $name, array $fields): void $params = []; foreach ($fields as $key => $value) { - $fixedKey = sprintf('%s%s', $name, $key); - $params[$fixedKey] = $value; + $params[$name . $key] = $value; } $button = sprintf('%s_submit', $name); diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 649bfcd9..8da45f54 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -164,13 +164,12 @@ public function getMailerEvent(int $index = 0, ?string $transport = null): ?Mess protected function getMessageMailerEvents(): MessageEvents { - $mailer = $this->getService('mailer.message_logger_listener'); - if ($mailer instanceof MessageLoggerListener) { - return $mailer->getEvents(); - } - $mailer = $this->getService('mailer.logger_message_listener'); - if ($mailer instanceof MessageLoggerListener) { - return $mailer->getEvents(); + $services = ['mailer.message_logger_listener', 'mailer.logger_message_listener']; + foreach ($services as $serviceId) { + $mailer = $this->getService($serviceId); + if ($mailer instanceof MessageLoggerListener) { + return $mailer->getEvents(); + } } Assert::fail("Emails can't be tested without Symfony Mailer service."); } diff --git a/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php index 77422c72..765d8d03 100644 --- a/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/NotifierAssertionsTrait.php @@ -251,13 +251,12 @@ protected function getNotificationEvents(): NotificationEvents Assert::fail('Notifier assertions require Symfony 6.2 or higher.'); } - $notifier = $this->getService('notifier.notification_logger_listener'); - if ($notifier instanceof NotificationLoggerListener) { - return $notifier->getEvents(); - } - $notifier = $this->getService('notifier.logger_notification_listener'); - if ($notifier instanceof NotificationLoggerListener) { - return $notifier->getEvents(); + $services = ['notifier.notification_logger_listener', 'notifier.logger_notification_listener']; + foreach ($services as $serviceId) { + $notifier = $this->getService($serviceId); + if ($notifier instanceof NotificationLoggerListener) { + return $notifier->getEvents(); + } } Assert::fail("Notifications can't be tested without Symfony Notifier service."); }