From 29fdc3c798d4c7f5fab89cbce4bc8a9f9880bec1 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves Date: Fri, 5 Jun 2026 18:49:58 -0500 Subject: [PATCH] Fix inconsistencies across assertion traits --- src/Codeception/Lib/Connector/Symfony.php | 38 ++++++---- src/Codeception/Module/Symfony.php | 11 +-- .../Module/Symfony/BrowserAssertionsTrait.php | 3 +- src/Codeception/Module/Symfony/CacheTrait.php | 5 +- .../Symfony/DoctrineAssertionsTrait.php | 17 +++-- .../Symfony/DomCrawlerAssertionsTrait.php | 4 +- .../Module/Symfony/FormAssertionsTrait.php | 70 +++++++++++-------- .../Symfony/HttpClientAssertionsTrait.php | 10 +-- .../Module/Symfony/RouterAssertionsTrait.php | 13 ++-- .../Module/Symfony/SessionAssertionsTrait.php | 22 +++--- .../Symfony/ValidatorAssertionsTrait.php | 15 ++-- tests/Support/CodeceptTestCase.php | 1 + 12 files changed, 123 insertions(+), 86 deletions(-) diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index 2e293228..2c52b5ce 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -53,11 +53,7 @@ protected function doRequest(object $request): Response */ public function rebootKernel(): void { - foreach ($this->persistentServices as $name => $_) { - if ($this->container->has($name)) { - $this->persistentServices[$name] = $this->container->get($name); - } - } + $this->updatePersistentServices(); $this->persistDoctrineConnections(); @@ -68,15 +64,7 @@ public function rebootKernel(): void $this->container = $this->resolveContainer(); - foreach ($this->persistentServices as $name => $service) { - try { - $this->container->set($name, $service); - } catch (InvalidArgumentException $e) { - if (function_exists('codecept_debug')) { - codecept_debug("[Symfony] Can't set persistent service {$name}: {$e->getMessage()}"); - } - } - } + $this->injectPersistentServices(); $this->getProfiler()?->enable(); } @@ -116,4 +104,26 @@ private function persistDoctrineConnections(): void } })->call($this->kernel->getContainer()); } + + private function updatePersistentServices(): void + { + foreach ($this->persistentServices as $name => $_) { + if ($this->container->has($name)) { + $this->persistentServices[$name] = $this->container->get($name); + } + } + } + + private function injectPersistentServices(): void + { + foreach ($this->persistentServices as $name => $service) { + try { + $this->container->set($name, $service); + } catch (InvalidArgumentException $e) { + if (function_exists('codecept_debug')) { + codecept_debug("[Symfony] Can't set persistent service {$name}: {$e->getMessage()}"); + } + } + } + } } diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index a94490a5..ba8ece35 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -41,7 +41,6 @@ use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Dotenv\Dotenv; -use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Profiler\Profile; @@ -57,6 +56,7 @@ use function count; use function extension_loaded; use function file_exists; +use function glob; use function implode; use function ini_get; use function ini_set; @@ -259,6 +259,7 @@ public function _after(TestInterface $test): void $this->cachedResponse = null; $this->cachedProfile = null; + $this->cachedRoutes = null; parent::_after($test); } @@ -333,8 +334,8 @@ protected function getKernelClass(): string if (file_exists($expectedKernelPath)) { include_once $expectedKernelPath; } else { - foreach ((new Finder())->name('*Kernel.php')->depth('0')->in($path) as $file) { - include_once $file->getRealPath(); + foreach (glob($path . DIRECTORY_SEPARATOR . '*Kernel.php') ?: [] as $file) { + include_once $file; } } @@ -450,12 +451,12 @@ private function debugSecurityData(SecurityDataCollector $securityCollector): vo private function debugMailerData(MessageDataCollector $messageCollector): void { - $this->debugSection('Emails', sprintf('%d sent', count($messageCollector->getEvents()->getMessages()))); + $this->debugSection('Emails', sprintf('%d sent', count($messageCollector->getEvents()->getEvents()))); } private function debugNotifierData(NotificationDataCollector $notificationCollector): void { - $this->debugSection('Notifications', sprintf('%d sent', count($notificationCollector->getEvents()->getMessages()))); + $this->debugSection('Notifications', sprintf('%d sent', count($notificationCollector->getEvents()->getEvents()))); } private function debugTimeData(TimeDataCollector $timeCollector): void diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 076e4349..e86a7dda 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -22,7 +22,6 @@ use Symfony\Component\HttpFoundation\Test\Constraint\ResponseStatusCodeSame; use function class_exists; -use function count; use function sprintf; trait BrowserAssertionsTrait @@ -372,7 +371,7 @@ public function submitSymfonyForm(string $name, array $fields): void } $node = $this->getClient()->getCrawler()->filter($selector); - $this->assertGreaterThan(0, count($node), sprintf('Form "%s" not found.', $selector)); + $this->assertGreaterThan(0, $node->count(), sprintf('Form "%s" not found.', $selector)); $form = $node->form(); $this->getClient()->submit($form, $params); } diff --git a/src/Codeception/Module/Symfony/CacheTrait.php b/src/Codeception/Module/Symfony/CacheTrait.php index d2a7de38..1b47b413 100644 --- a/src/Codeception/Module/Symfony/CacheTrait.php +++ b/src/Codeception/Module/Symfony/CacheTrait.php @@ -14,6 +14,8 @@ trait CacheTrait { private ?object $cachedResponse = null; private ?Profile $cachedProfile = null; + /** @var array|null */ + protected ?array $cachedRoutes = null; /** @var array */ protected array $state = []; @@ -53,7 +55,8 @@ protected function getInternalDomains(): array protected function clearRouterCache(): void { - unset($this->state['internalDomains'], $this->state['cachedRoutes']); + unset($this->state['internalDomains']); + $this->cachedRoutes = null; } /** diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php index 74f5496a..e40617e2 100644 --- a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php @@ -24,7 +24,8 @@ trait DoctrineAssertionsTrait * $I->grabNumRecords(User::class, ['status' => 'active']); * ``` * - * @param class-string $entityClass Fully-qualified entity class name + * @template T of object + * @param class-string $entityClass Fully-qualified entity class name * @param array $criteria Optional query criteria */ public function grabNumRecords(string $entityClass, array $criteria = []): int @@ -44,18 +45,20 @@ public function grabNumRecords(string $entityClass, array $criteria = []): int * $I->grabRepository(UserRepositoryInterface::class); // interface * ``` * - * @param object|class-string $mixed - * @return EntityRepository + * @template T of object + * @param object|class-string $entityOrClass + * @return ($entityOrClass is class-string ? EntityRepository : EntityRepository) */ - public function grabRepository(object|string $mixed): EntityRepository + public function grabRepository(object|string $entityOrClass): EntityRepository { - $id = is_object($mixed) ? $mixed::class : $mixed; + $id = is_object($entityOrClass) ? $entityOrClass::class : $entityOrClass; if (interface_exists($id) || is_subclass_of($id, EntityRepository::class)) { $repo = $this->grabService($id); if (!($repo instanceof EntityRepository && $repo instanceof $id)) { Assert::fail(sprintf("'%s' is not an entity repository", $id)); } + /** @var EntityRepository|EntityRepository $repo */ return $repo; } @@ -64,6 +67,7 @@ public function grabRepository(object|string $mixed): EntityRepository Assert::fail(sprintf("'%s' is not a managed Doctrine entity", $id)); } + /** @var EntityRepository|EntityRepository */ return $em->getRepository($id); } @@ -77,8 +81,9 @@ public function grabRepository(object|string $mixed): EntityRepository * $I->seeNumRecords(80, User::class); * ``` * + * @template T of object * @param int $expectedNum Expected count - * @param class-string $className Entity class + * @param class-string $className Entity class * @param array $criteria Optional criteria */ public function seeNumRecords(int $expectedNum, string $className, array $criteria = []): void diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index a08b4f23..8a03bfc7 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -170,10 +170,10 @@ private function assertCheckboxState(string $fieldName, bool $checked, string $m $this->assertThatCrawler($constraint, $message); } - private function assertInputValue(string $fieldName, string $value, bool $same, string $message): void + private function assertInputValue(string $fieldName, string $expectedValue, bool $same, string $message): void { $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message); - $constraint = new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $value); + $constraint = new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue); if (!$same) { $constraint = new LogicalNot($constraint); } diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 23688db5..59bf121c 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -8,7 +8,6 @@ use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; use Symfony\Component\VarDumper\Cloner\Data; -use function count; use function implode; use function is_array; use function is_int; @@ -26,10 +25,10 @@ trait FormAssertionsTrait * $I->assertFormValue('#loginForm', 'username', 'john_doe'); * ``` */ - public function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void + public function assertFormValue(string $formSelector, string $fieldName, string $expectedValue, string $message = ''): void { $node = $this->getClient()->getCrawler()->filter($formSelector); - $this->assertGreaterThan(0, count($node), sprintf('Form "%s" not found.', $formSelector)); + $this->assertGreaterThan(0, $node->count(), sprintf('Form "%s" not found.', $formSelector)); $values = $node->form()->getValues(); $this->assertArrayHasKey( @@ -37,7 +36,7 @@ public function assertFormValue(string $formSelector, string $fieldName, string $values, $message ?: sprintf('Field "%s" not found in form "%s".', $fieldName, $formSelector) ); - $this->assertSame($value, $values[$fieldName]); + $this->assertSame($expectedValue, $values[$fieldName]); } /** @@ -51,7 +50,7 @@ public function assertFormValue(string $formSelector, string $fieldName, string public function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void { $node = $this->getClient()->getCrawler()->filter($formSelector); - $this->assertGreaterThan(0, count($node), sprintf('Form "%s" not found.', $formSelector)); + $this->assertGreaterThan(0, $node->count(), sprintf('Form "%s" not found.', $formSelector)); $values = $node->form()->getValues(); $this->assertArrayNotHasKey( @@ -86,19 +85,11 @@ public function dontSeeFormErrors(): void */ public function seeFormErrorMessage(string $field, ?string $message = null): void { - $errors = $this->getErrorsForField($field); - - if ($errors === []) { - Assert::fail("No form error message for field '{$field}'."); - } + $collector = $this->grabFormCollector(__FUNCTION__); + /** @var array $formsData */ + $formsData = $this->getRawCollectorData($collector)['forms'] ?? []; - if ($message !== null) { - $this->assertStringContainsString( - $message, - implode("\n", $errors), - sprintf("There is an error message for the field '%s', but it does not match the expected message.", $field) - ); - } + $this->assertFormErrorMessage($field, $message, $formsData); } /** @@ -140,8 +131,34 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi */ public function seeFormErrorMessages(array $expectedErrors): void { + $collector = $this->grabFormCollector(__FUNCTION__); + /** @var array $formsData */ + $formsData = $this->getRawCollectorData($collector)['forms'] ?? []; + foreach ($expectedErrors as $field => $msg) { - is_int($field) ? $this->seeFormErrorMessage((string) $msg) : $this->seeFormErrorMessage($field, $msg); + if (is_int($field)) { + $this->assertFormErrorMessage((string) $msg, null, $formsData); + } else { + $this->assertFormErrorMessage($field, $msg, $formsData); + } + } + } + + /** @param array $formsData */ + private function assertFormErrorMessage(string $field, ?string $message, array $formsData): void + { + $errors = $this->getErrorsForField($field, $formsData); + + if ($errors === []) { + Assert::fail("No form error message for field '{$field}'."); + } + + if ($message !== null) { + $this->assertStringContainsString( + $message, + implode("\n", $errors), + sprintf("There is an error message for the field '%s', but it does not match the expected message.", $field) + ); } } @@ -172,16 +189,11 @@ private function getFormErrorsCount(string $function): int } /** + * @param array $formsData * @return list */ - private function getErrorsForField(string $field): array + private function getErrorsForField(string $field, array $formsData): array { - $collector = $this->grabFormCollector('seeFormErrorMessage'); - $formsData = $this->getRawCollectorData($collector)['forms'] ?? []; - if (!is_array($formsData)) { - return []; - } - $errorsForField = []; $fieldFound = false; @@ -215,11 +227,11 @@ private function getErrorsForField(string $field): array /** @return array */ private function getRawCollectorData(FormDataCollector $collector): array { - $data = $collector->getData(); - if ($data instanceof Data) { - $data = $data->getValue(true); + $collectorData = $collector->getData(); + if ($collectorData instanceof Data) { + $collectorData = $collectorData->getValue(true); } /** @var array */ - return is_array($data) ? $data : []; + return is_array($collectorData) ? $collectorData : []; } } diff --git a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php index c99deb43..89b19118 100644 --- a/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php @@ -145,13 +145,13 @@ private function getHttpClientTraces(string $httpClientId, string $function): ar return $clientData['traces']; } - private function extractValue(mixed $value): mixed + private function extractValue(mixed $traceData): mixed { return match (true) { - $value instanceof Data => $value->getValue(true), - is_object($value) && method_exists($value, 'getValue') => $value->getValue(true), - $value instanceof Stringable => (string) $value, - default => $value, + $traceData instanceof Data => $traceData->getValue(true), + is_object($traceData) && method_exists($traceData, 'getValue') => $traceData->getValue(true), + $traceData instanceof Stringable => (string) $traceData, + default => $traceData, }; } diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index 0a6da57d..3550301d 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -118,9 +118,13 @@ private function getCurrentRouteMatch(string $routeName): array private function findRouteByActionOrFail(string $action): string { + if (isset($this->cachedRoutes[$action])) { + return $this->cachedRoutes[$action]; + } + foreach ($this->getCachedRoutes() as $ctrl => $name) { if (str_ends_with($ctrl, $action)) { - return $name; + return $this->cachedRoutes[$action] = $name; } } @@ -130,9 +134,8 @@ private function findRouteByActionOrFail(string $action): string /** @return array */ private function getCachedRoutes(): array { - if (isset($this->state['cachedRoutes'])) { - /** @var array */ - return $this->state['cachedRoutes']; + if ($this->cachedRoutes !== null) { + return $this->cachedRoutes; } $routes = []; @@ -143,7 +146,7 @@ private function getCachedRoutes(): array } } - return $this->state['cachedRoutes'] = $routes; + return $this->cachedRoutes = $routes; } private function assertRouteExists(string $routeName): void diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index 2486a343..57d42b4c 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -74,12 +74,12 @@ public function amLoggedInWithToken(TokenInterface $token, string $firewallName * $I->dontSeeInSession('attribute', 'value'); * ``` */ - public function dontSeeInSession(string $attribute, mixed $value = null): void + public function dontSeeInSession(string $attribute, mixed $expectedValue = null): void { $session = $this->getCurrentSession(); - $value === null + $expectedValue === null ? $this->assertFalse($session->has($attribute), "Session attribute '{$attribute}' exists.") - : $this->assertNotSame($value, $session->get($attribute)); + : $this->assertNotSame($expectedValue, $session->get($attribute)); } /** @@ -141,13 +141,13 @@ public function logoutProgrammatically(): void * $I->seeInSession('attribute', 'value'); * ``` */ - public function seeInSession(string $attribute, mixed $value = null): void + public function seeInSession(string $attribute, mixed $expectedValue = null): void { $session = $this->getCurrentSession(); $this->assertTrue($session->has($attribute), "No session attribute with name '{$attribute}'"); - if ($value !== null) { - $this->assertSame($value, $session->get($attribute)); + if ($expectedValue !== null) { + $this->assertSame($expectedValue, $session->get($attribute)); } } @@ -166,16 +166,16 @@ public function seeSessionHasValues(array $bindings): void { $session = $this->getCurrentSession(); - foreach ($bindings as $key => $value) { + foreach ($bindings as $key => $expectedAttr) { if (!is_int($key)) { $this->assertTrue($session->has($key), "No session attribute with name '{$key}'"); - $this->assertSame($value, $session->get($key)); + $this->assertSame($expectedAttr, $session->get($key)); continue; } - if (!is_string($value)) { - throw new InvalidArgumentException(sprintf('Attribute name must be string, %s given.', get_debug_type($value))); + if (!is_string($expectedAttr)) { + throw new InvalidArgumentException(sprintf('Attribute name must be string, %s given.', get_debug_type($expectedAttr))); } - $this->assertTrue($session->has($value), "No session attribute with name '{$value}'"); + $this->assertTrue($session->has($expectedAttr), "No session attribute with name '{$expectedAttr}'"); } } diff --git a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php index ac7a0035..e516a5f1 100644 --- a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php @@ -7,7 +7,6 @@ use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use function array_filter; use function iterator_to_array; use function str_contains; @@ -90,14 +89,18 @@ protected function getViolationsForSubject(object $subject, ?string $propertyPat $validator = $this->getValidatorService(); $violations = $propertyPath ? $validator->validateProperty($subject, $propertyPath) : $validator->validate($subject); + /** @var ConstraintViolationInterface[] $violations */ $violations = iterator_to_array($violations); if ($constraint !== null) { - return (array) array_filter( - $violations, - static fn(ConstraintViolationInterface $violation): bool => $violation->getConstraint() !== null - && $violation->getConstraint()::class === $constraint - ); + $filteredViolations = []; + foreach ($violations as $violation) { + $violationConstraint = $violation->getConstraint(); + if ($violationConstraint !== null && $violationConstraint::class === $constraint) { + $filteredViolations[] = $violation; + } + } + return $filteredViolations; } return $violations; diff --git a/tests/Support/CodeceptTestCase.php b/tests/Support/CodeceptTestCase.php index d657aed0..00526a36 100644 --- a/tests/Support/CodeceptTestCase.php +++ b/tests/Support/CodeceptTestCase.php @@ -63,6 +63,7 @@ protected function tearDown(): void $this->cachedResponse = null; $this->cachedProfile = null; + $this->cachedRoutes = null; $this->restoreErrorHandler(); parent::tearDown();