From e2a0f4dda26f88a3a396a13a4dbf4582a5f8f368 Mon Sep 17 00:00:00 2001 From: Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 30 May 2022 09:44:05 -0500 Subject: [PATCH 01/44] Improved support for Codeception 5/PHP 8 (#159) Co-authored-by: Dan Barrett --- .github/workflows/main.yml | 13 ++++++++++++- composer.json | 18 +++++++++++------- readme.md | 2 +- src/Codeception/Module/Symfony.php | 3 +++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9ef116b..be8e3c2f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,10 @@ jobs: strategy: matrix: php: [8.0, 8.1] - symfony: ["4.4.*", "5.4.*", "6.0.*"] + symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*"] + exclude: + - php: 8.0 + symfony: "6.1.*" steps: - name: Checkout code @@ -47,6 +50,14 @@ jobs: path: framework-tests ref: "6.0" + - name: Checkout Symfony 6.1 Sample + if: "matrix.symfony == '6.1.*' && matrix.php == '8.1'" + uses: actions/checkout@v2 + with: + repository: Codeception/symfony-module-tests + path: framework-tests + ref: "6.1" + - name: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" diff --git a/composer.json b/composer.json index 0363fe31..74e19855 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,12 @@ { "name": "codeception/module-symfony", "description": "Codeception module for Symfony framework", - "keywords": ["codeception", "symfony"], - "homepage": "https://codeception.com/", - "type": "library", "license": "MIT", + "type": "library", + "keywords": [ + "codeception", + "symfony" + ], "authors": [ { "name": "Michael Bodnarchuk" @@ -14,12 +16,12 @@ "homepage": "https://medium.com/@ganieves" } ], - "minimum-stability": "dev", + "homepage": "https://codeception.com/", "require": { "php": "^8.0", "ext-json": "*", - "codeception/lib-innerbrowser": "^3.1.1", - "codeception/codeception": "^5.0.0-RC3" + "codeception/codeception": "^5.0.0-RC3", + "codeception/lib-innerbrowser": "^3.1.1" }, "require-dev": { "codeception/module-asserts": "^3.0", @@ -38,10 +40,12 @@ "codeception/module-asserts": "Include traditional PHPUnit assertions in your tests", "symfony/web-profiler-bundle": "Tool that gives information about the execution of requests" }, + "minimum-stability": "RC", "autoload": { "classmap": ["src/"] }, "config": { - "classmap-authoritative": true + "classmap-authoritative": true, + "sort-packages": true } } diff --git a/readme.md b/readme.md index 26b930c6..c832d43a 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,7 @@ A Codeception module for Symfony framework. ## Requirements -* `Symfony 4.4` or higher. +* `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. ## Installation diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index cdb206cc..018f8710 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -154,6 +154,9 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule */ public ?AbstractBrowser $client = null; + /** + * @var array + */ public array $config = [ 'app_path' => 'app', 'kernel_class' => 'App\Kernel', From c8bea5db325ea0682313fb67102b06f395cf4c1a Mon Sep 17 00:00:00 2001 From: Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Mon, 30 May 2022 11:32:13 -0500 Subject: [PATCH 02/44] Improved support for PHP 8.0 (#160) --- src/Codeception/Lib/Connector/Symfony.php | 2 +- src/Codeception/Module/Symfony.php | 11 +--------- .../Symfony/DoctrineAssertionsTrait.php | 8 ++----- .../Module/Symfony/EventsAssertionsTrait.php | 22 +++++++++---------- .../Module/Symfony/FormAssertionsTrait.php | 2 +- .../Module/Symfony/MailerAssertionsTrait.php | 2 -- .../Module/Symfony/RouterAssertionsTrait.php | 8 +++---- .../Symfony/SecurityAssertionsTrait.php | 5 +---- .../Symfony/ServicesAssertionsTrait.php | 4 ---- .../Module/Symfony/SessionAssertionsTrait.php | 9 ++------ .../Module/Symfony/TimeAssertionsTrait.php | 2 +- 11 files changed, 24 insertions(+), 51 deletions(-) diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index 557a9434..dafcaa5a 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -38,7 +38,7 @@ class Symfony extends HttpKernelBrowser public function __construct(Kernel $kernel, array $services = [], bool $rebootable = true) { parent::__construct($kernel); - $this->followRedirects(true); + $this->followRedirects(); $this->rebootable = $rebootable; $this->persistentServices = $services; $this->container = $this->getContainer(); diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 018f8710..abcd56e9 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -212,8 +212,6 @@ public function _initialize(): void /** * Initialize new client instance before each test - * - * @param TestInterface $test */ public function _before(TestInterface $test): void { @@ -223,8 +221,6 @@ public function _before(TestInterface $test): void /** * Update permanent services after each test - * - * @param TestInterface $test */ public function _after(TestInterface $test): void { @@ -370,13 +366,8 @@ protected function getProfile(): ?Profile /** * Grabs a Symfony Data Collector - * - * @param string $collector - * @param string $function - * @param string|null $message - * @return DataCollectorInterface */ - protected function grabCollector(string $collector, string $function, ?string $message = null): DataCollectorInterface + protected function grabCollector(string $collector, string $function, string $message = null): DataCollectorInterface { if (($profile = $this->getProfile()) === null) { $this->fail( diff --git a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php index 2ea3606f..9aa4a054 100644 --- a/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DoctrineAssertionsTrait.php @@ -27,7 +27,6 @@ trait DoctrineAssertionsTrait * * @param string $entityClass The entity class * @param array $criteria Optional query criteria - * @return int */ public function grabNumRecords(string $entityClass, array $criteria = []): int { @@ -55,11 +54,8 @@ public function grabNumRecords(string $entityClass, array $criteria = []): int * $I->grabRepository(UserRepository::class); * $I->grabRepository(UserRepositoryInterface::class); * ``` - * - * @param object|string $mixed - * @return \Doctrine\ORM\EntityRepository|null */ - public function grabRepository($mixed): ?EntityRepository + public function grabRepository(object|string $mixed): ?EntityRepository { $entityRepoClass = EntityRepository::class; $isNotARepo = function () use ($mixed): void { @@ -79,7 +75,7 @@ public function grabRepository($mixed): ?EntityRepository }; if (is_object($mixed)) { - $mixed = get_class($mixed); + $mixed = $mixed::class; } if (interface_exists($mixed)) { diff --git a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php index 64eca663..f4b47b98 100644 --- a/src/Codeception/Module/Symfony/EventsAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/EventsAssertionsTrait.php @@ -28,9 +28,9 @@ trait EventsAssertionsTrait * $I->dontSeeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param string|object|string[] $expected + * @param object|string|string[] $expected */ - public function dontSeeOrphanEvent($expected = null): void + public function dontSeeOrphanEvent(array|object|string $expected = null): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -55,9 +55,9 @@ public function dontSeeOrphanEvent($expected = null): void * $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param string|object|string[] $expected + * @param object|string|string[] $expected */ - public function dontSeeEventTriggered($expected): void + public function dontSeeEventTriggered(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -82,9 +82,9 @@ public function dontSeeEventTriggered($expected): void * $I->seeOrphanEvent(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param string|object|string[] $expected + * @param object|string|string[] $expected */ - public function seeOrphanEvent($expected): void + public function seeOrphanEvent(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -105,9 +105,9 @@ public function seeOrphanEvent($expected): void * $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); * ``` * - * @param string|object|string[] $expected + * @param object|string|string[] $expected */ - public function seeEventTriggered($expected): void + public function seeEventTriggered(array|object|string $expected): void { $eventCollector = $this->grabEventCollector(__FUNCTION__); @@ -123,7 +123,7 @@ protected function assertEventNotTriggered(Data $data, array $expected): void $actual = $data->getValue(true); foreach ($expected as $expectedEvent) { - $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; + $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent; $this->assertFalse( $this->eventWasTriggered($actual, (string)$expectedEvent), "The '{$expectedEvent}' event triggered" @@ -140,7 +140,7 @@ protected function assertEventTriggered(Data $data, array $expected): void $actual = $data->getValue(true); foreach ($expected as $expectedEvent) { - $expectedEvent = is_object($expectedEvent) ? get_class($expectedEvent) : $expectedEvent; + $expectedEvent = is_object($expectedEvent) ? $expectedEvent::class : $expectedEvent; $this->assertTrue( $this->eventWasTriggered($actual, (string)$expectedEvent), "The '{$expectedEvent}' event did not trigger" @@ -154,7 +154,7 @@ protected function eventWasTriggered(array $actual, string $expectedEvent): bool foreach ($actual as $actualEvent) { if (is_array($actualEvent)) { // Called Listeners - if (strpos($actualEvent['pretty'], $expectedEvent) === 0) { + if (str_starts_with($actualEvent['pretty'], $expectedEvent)) { $triggered = true; } } else { // Orphan Events diff --git a/src/Codeception/Module/Symfony/FormAssertionsTrait.php b/src/Codeception/Module/Symfony/FormAssertionsTrait.php index 3b923059..8261ea9f 100644 --- a/src/Codeception/Module/Symfony/FormAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/FormAssertionsTrait.php @@ -46,7 +46,7 @@ public function dontSeeFormErrors(): void * @param string $field * @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/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 9ead87ff..cb82484a 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -53,8 +53,6 @@ public function seeEmailIsSent(int $expectedCount = 1): void * $address = $email->getTo()[0]; * $I->assertSame('john_doe@example.com', $address->getAddress()); * ``` - * - * @return \Symfony\Component\Mime\Email|null */ public function grabLastSentEmail(): ?Email { diff --git a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php index 4ca8032d..c023782f 100644 --- a/src/Codeception/Module/Symfony/RouterAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/RouterAssertionsTrait.php @@ -37,7 +37,7 @@ public function amOnAction(string $action, array $params = []): void foreach ($routes as $route) { $controller = $route->getDefault('_controller'); - if (substr_compare($controller, $action, -strlen($action)) === 0) { + if (str_ends_with($controller, $action)) { $resource = $router->match($route->getPath()); $url = $router->generate( $resource['_route'], @@ -100,7 +100,7 @@ public function seeCurrentActionIs(string $action): void foreach ($routes as $route) { $controller = $route->getDefault('_controller'); - if (substr_compare($controller, $action, -strlen($action)) === 0) { + if (str_ends_with($controller, $action)) { $request = $this->getClient()->getRequest(); $currentActionFqcn = $request->attributes->get('_controller'); @@ -135,7 +135,7 @@ public function seeCurrentRouteIs(string $routeName, array $params = []): void $match = []; try { $match = $router->match($uri); - } catch (ResourceNotFoundException $e) { + } catch (ResourceNotFoundException) { $this->fail(sprintf('The "%s" url does not match with any route', $uri)); } @@ -167,7 +167,7 @@ public function seeInCurrentRoute(string $routeName): void $matchedRouteName = ''; try { $matchedRouteName = (string)$router->match($uri)['_route']; - } catch (ResourceNotFoundException $e) { + } catch (ResourceNotFoundException) { $this->fail(sprintf('The "%s" url does not match with any route', $uri)); } diff --git a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php index a8ad24b6..a07e3ab3 100644 --- a/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SecurityAssertionsTrait.php @@ -183,10 +183,7 @@ protected function grabSecurityService(): Security return $this->grabService('security.helper'); } - /** - * @return UserPasswordHasherInterface|UserPasswordEncoderInterface - */ - protected function grabPasswordHasherService() + protected function grabPasswordHasherService(): UserPasswordHasherInterface|UserPasswordEncoderInterface { $hasher = $this->getService('security.password_hasher') ?: $this->getService('security.password_encoder'); diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php index c21289c5..b53a72b8 100644 --- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -21,7 +21,6 @@ trait ServicesAssertionsTrait * * @part services * @param string $serviceId - * @return object */ public function grabService(string $serviceId): object { @@ -37,7 +36,6 @@ public function grabService(string $serviceId): object * Get service $serviceName and add it to the lists of persistent services. * * @part services - * @param string $serviceName */ public function persistService(string $serviceName): void { @@ -53,7 +51,6 @@ public function persistService(string $serviceName): void * making that service persistent between tests. * * @part services - * @param string $serviceName */ public function persistPermanentService(string $serviceName): void { @@ -69,7 +66,6 @@ public function persistPermanentService(string $serviceName): void * Remove service $serviceName from the lists of persistent services. * * @part services - * @param string $serviceName */ public function unpersistService(string $serviceName): void { diff --git a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php index e7ff9cbb..f6911303 100644 --- a/src/Codeception/Module/Symfony/SessionAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/SessionAssertionsTrait.php @@ -75,10 +75,8 @@ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main', * $I->dontSeeInSession('attribute', 'value'); * ``` * - * @param string $attribute - * @param mixed|null $value */ - public function dontSeeInSession(string $attribute, $value = null): void + public function dontSeeInSession(string $attribute, mixed $value = null): void { $session = $this->getCurrentSession(); @@ -160,11 +158,8 @@ public function logoutProgrammatically(): void * $I->seeInSession('attribute'); * $I->seeInSession('attribute', 'value'); * ``` - * - * @param string $attribute - * @param mixed|null $value */ - public function seeInSession(string $attribute, $value = null): void + public function seeInSession(string $attribute, mixed $value = null): void { $session = $this->getCurrentSession(); diff --git a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php index ad3c1862..d48222eb 100644 --- a/src/Codeception/Module/Symfony/TimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TimeAssertionsTrait.php @@ -24,7 +24,7 @@ trait TimeAssertionsTrait * * @param int|float $expectedMilliseconds The expected time in milliseconds */ - public function seeRequestTimeIsLessThan($expectedMilliseconds): void + public function seeRequestTimeIsLessThan(int|float $expectedMilliseconds): void { $expectedMilliseconds = round($expectedMilliseconds, 2); From ac0c67a8d1e2a06d4c6e534bea4fc084327ba00b Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Mon, 5 Sep 2022 23:58:09 +0200 Subject: [PATCH 03/44] Improving language --- 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 abcd56e9..fc8044ca 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -305,7 +305,7 @@ protected function getKernelClass(): string throw new ModuleRequireException( self::class, "Can't load Kernel from {$path}.\n" - . 'Directory does not exists. Use `app_path` parameter to provide valid application path' + . 'Directory does not exist. Set `app_path` in your suite configuration to a valid application path.' ); } From 36764aa7a9596fada116847bb98ff346228530bf Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Wed, 7 Sep 2022 09:17:04 +0300 Subject: [PATCH 04/44] CI: Remove conflicting codeception version from framework-tests --- .github/workflows/main.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be8e3c2f..b459ea2b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,17 +71,17 @@ jobs: - name: Install dependencies run: | - composer require symfony/finder=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/yaml=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/console=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/event-dispatcher=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/css-selector=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/dom-crawler=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require symfony/browser-kit=${{ matrix.symfony }} --ignore-platform-req=php --no-update - composer require vlucas/phpdotenv --ignore-platform-req=php --no-update - composer require codeception/module-asserts="3.*" --ignore-platform-req=php --no-update - composer require codeception/module-doctrine2="3.*" --ignore-platform-req=php --no-update - composer install --prefer-dist --no-progress --ignore-platform-req=php --no-dev + composer require symfony/finder=${{ matrix.symfony }} --no-update + composer require symfony/yaml=${{ matrix.symfony }} --no-update + composer require symfony/console=${{ matrix.symfony }} --no-update + composer require symfony/event-dispatcher=${{ matrix.symfony }} --no-update + composer require symfony/css-selector=${{ matrix.symfony }} --no-update + composer require symfony/dom-crawler=${{ matrix.symfony }} --no-update + 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 update --prefer-dist --no-progress --no-dev - name: Validate composer.json and composer.lock run: composer validate @@ -89,8 +89,8 @@ jobs: - name: Install Symfony Sample run: | - composer remove codeception/module-symfony --dev --no-update --ignore-platform-req=php - composer install --no-progress --ignore-platform-req=php + composer remove codeception/codeception codeception/module-asserts codeception/module-doctrine2 codeception/lib-innerbrowser codeception/module-symfony --dev --no-update + composer update --no-progress working-directory: framework-tests - name: Prepare the test environment From 5f7b5c007433aabe4eebf28a427773605da32f02 Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Wed, 7 Sep 2022 12:02:36 +0300 Subject: [PATCH 05/44] Run tests on PHP 8.2 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b459ea2b..a702584e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [8.0, 8.1] + php: [8.0, 8.1, 8.2] symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*"] exclude: - php: 8.0 @@ -51,7 +51,7 @@ jobs: ref: "6.0" - name: Checkout Symfony 6.1 Sample - if: "matrix.symfony == '6.1.*' && matrix.php == '8.1'" + if: "matrix.symfony == '6.1.*'" uses: actions/checkout@v2 with: repository: Codeception/symfony-module-tests From 4f7edfaaa59ca1ba99424ace79997591c2ba9281 Mon Sep 17 00:00:00 2001 From: Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Sun, 8 Jan 2023 00:42:00 -0500 Subject: [PATCH 06/44] Update to Symfony 6.2 --- .github/workflows/main.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a702584e..ab42b8c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,10 +9,12 @@ jobs: strategy: matrix: php: [8.0, 8.1, 8.2] - symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*"] + symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*", "6.2.*"] exclude: - php: 8.0 symfony: "6.1.*" + - php: 8.0 + symfony: "6.2.*" steps: - name: Checkout code @@ -58,6 +60,14 @@ jobs: 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: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" From a5524bb7064cdbcd7e88406d523c068fb94105c5 Mon Sep 17 00:00:00 2001 From: W0rma Date: Mon, 6 Feb 2023 08:17:31 +0100 Subject: [PATCH 07/44] Allow installation of codeception/lib-innerbrowser v4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 74e19855..1e03edcd 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "php": "^8.0", "ext-json": "*", "codeception/codeception": "^5.0.0-RC3", - "codeception/lib-innerbrowser": "^3.1.1" + "codeception/lib-innerbrowser": "^3.1.1 | ^4.0" }, "require-dev": { "codeception/module-asserts": "^3.0", From 6bf04ab3f03c752b9168b011f2c8d257b06a6a0b Mon Sep 17 00:00:00 2001 From: Gintautas Miselis Date: Mon, 13 Feb 2023 09:21:01 +0200 Subject: [PATCH 08/44] 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 09/44] 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 10/44] 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 11/44] 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 12/44] 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 13/44] 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 14/44] 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 15/44] 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 16/44] 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 17/44] 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 18/44] 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 19/44] 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 20/44] 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 21/44] 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 22/44] 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 23/44] 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 24/44] 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 25/44] 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 26/44] 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 27/44] #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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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 41/44] 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 From c3a1c42f8dfe15e049bfc6763a962371a93e376b Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves <64917965+TavoNiievez@users.noreply.github.com> Date: Wed, 28 May 2025 13:25:49 -0500 Subject: [PATCH 42/44] Remove PHP 8.1 Support (#211) --- .github/workflows/main.yml | 2 +- composer.json | 19 +++-- .../Module/Symfony/BrowserAssertionsTrait.php | 9 ++- .../Module/Symfony/ConsoleAssertionsTrait.php | 74 ++++++++++--------- .../Symfony/DomCrawlerAssertionsTrait.php | 16 ++-- .../Module/Symfony/MimeAssertionsTrait.php | 6 +- .../Symfony/ServicesAssertionsTrait.php | 7 +- .../Symfony/TranslationAssertionsTrait.php | 2 +- .../Module/Symfony/TwigAssertionsTrait.php | 2 +- .../Symfony/ValidatorAssertionsTrait.php | 16 ++-- 10 files changed, 84 insertions(+), 69 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85b0d49d..43762ad0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [8.1, 8.2, 8.3, 8.4] + php: [8.2, 8.3, 8.4] symfony: ["5.4.*", "6.4.*", "7.2.*"] exclude: - php: 8.1 diff --git a/composer.json b/composer.json index 747a5941..f8023eb7 100644 --- a/composer.json +++ b/composer.json @@ -1,13 +1,17 @@ { "name": "codeception/module-symfony", "description": "Codeception module for Symfony framework", - "license": "MIT", "type": "library", + "license": "MIT", "keywords": [ "codeception", "functional testing", "symfony" ], + "homepage": "https://codeception.com/", + "support": { + "docs": "https://codeception.com/docs/modules/Symfony" + }, "authors": [ { "name": "Michael Bodnarchuk" @@ -17,17 +21,16 @@ "homepage": "https://medium.com/@ganieves" } ], - "homepage": "https://codeception.com/", "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", - "codeception/codeception": "^5.0.8", - "codeception/lib-innerbrowser": "^3.1.1 | ^4.0" + "codeception/codeception": "^5.3", + "codeception/lib-innerbrowser": "^3.1 | ^4.0" }, "require-dev": { "codeception/module-asserts": "^3.0", "codeception/module-doctrine": "^3.1", - "doctrine/orm": "^2.10", + "doctrine/orm": "^2.20", "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", @@ -62,12 +65,12 @@ "codeception/module-asserts": "Include traditional PHPUnit assertions in your tests", "symfony/web-profiler-bundle": "Tool that gives information about the execution of requests" }, - "minimum-stability": "RC", "autoload": { "classmap": ["src/"] }, "config": { "classmap-authoritative": true, "sort-packages": true - } + }, + "minimum-stability": "RC" } diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index fbd8a075..760f6cc7 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -258,13 +258,16 @@ public function assertResponseStatusCodeSame(int $expectedCode, string $message * <?php * $I->assertRouteSame('profile', ['id' => 123]); * ``` + * + * @param array<string, bool|float|int|null|string> $parameters */ - public function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void { + 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) { - $this->assertThat($request, new RequestAttributeValueSame($key, $value), $message); + $this->assertThat($request, new RequestAttributeValueSame($key, (string)$value), $message); } } @@ -349,7 +352,7 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void * ]); * ``` * - * @param string $name The `name` attribute of the `<form>`. You cannot use an array as a selector here. + * @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/ConsoleAssertionsTrait.php b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php index 18e20173..763f977e 100644 --- a/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php @@ -9,6 +9,9 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\HttpKernel\KernelInterface; +use function in_array; +use function sprintf; + trait ConsoleAssertionsTrait { /** @@ -20,40 +23,42 @@ trait ConsoleAssertionsTrait * $result = $I->runSymfonyConsoleCommand('hello:world', ['arg' => 'argValue', 'opt1' => 'optValue'], ['input']); * ``` * - * @param string $command The console command to execute - * @param array $parameters Parameters (arguments and options) to pass to the command - * @param array $consoleInputs Console inputs (e.g. used for interactive questions) - * @param int $expectedExitCode The expected exit code of the command - * @return string Returns the console output of the command + * @param string $command The console command to execute. + * @param array<string, int|string> $parameters Arguments and options passed to the command + * @param list<string> $consoleInputs Inputs for interactive questions. + * @param int $expectedExitCode Expected exit code. + * @return string Console output (stdout). */ - public function runSymfonyConsoleCommand(string $command, array $parameters = [], array $consoleInputs = [], int $expectedExitCode = 0): string - { - $kernel = $this->grabKernelService(); - $application = new Application($kernel); - $consoleCommand = $application->find($command); - $commandTester = new CommandTester($consoleCommand); + public function runSymfonyConsoleCommand( + string $command, + array $parameters = [], + array $consoleInputs = [], + int $expectedExitCode = 0 + ): string { + $kernel = $this->grabKernelService(); + $application = new Application($kernel); + $consoleCommand = $application->find($command); + $commandTester = new CommandTester($consoleCommand); $commandTester->setInputs($consoleInputs); - $input = ['command' => $command] + $parameters; - $options = $this->configureOptions($parameters); - + $input = ['command' => $command] + $parameters; + $options = $this->configureOptions($parameters); $exitCode = $commandTester->execute($input, $options); - $output = $commandTester->getDisplay(); + $output = $commandTester->getDisplay(); $this->assertSame( $expectedExitCode, $exitCode, - sprintf( - 'Command did not exit with code %d but with %d: %s', - $expectedExitCode, - $exitCode, - $output - ) + sprintf('Command exited with %d instead of expected %d. Output: %s', $exitCode, $expectedExitCode, $output) ); return $output; } + /** + * @param array<string, int|string|bool> $parameters + * @return array<string, mixed> Options array supported by CommandTester. + */ private function configureOptions(array $parameters): array { $options = []; @@ -69,27 +74,24 @@ private function configureOptions(array $parameters): array } if (in_array('--quiet', $parameters, true) || in_array('-q', $parameters, true)) { - $options['verbosity'] = OutputInterface::VERBOSITY_QUIET; + $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) + 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) + } 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) + } 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; } @@ -101,4 +103,4 @@ protected function grabKernelService(): KernelInterface { return $this->grabService('kernel'); } -} \ No newline at end of file +} diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index 8786be4c..f25f9bf3 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -36,9 +36,11 @@ public function assertCheckboxChecked(string $fieldName, string $message = ''): */ public function assertCheckboxNotChecked(string $fieldName, string $message = ''): void { - $this->assertThatCrawler(new LogicalNot( - new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked") - ), $message); + $this->assertThatCrawler( + new LogicalNot( + new CrawlerSelectorExists("input[name=\"$fieldName\"]:checked") + ), $message + ); } /** @@ -52,9 +54,11 @@ public function assertCheckboxNotChecked(string $fieldName, string $message = '' public function assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = ''): void { $this->assertThatCrawler(new CrawlerSelectorExists("input[name=\"$fieldName\"]"), $message); - $this->assertThatCrawler(new LogicalNot( - new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) - ), $message); + $this->assertThatCrawler( + new LogicalNot( + new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'value', $expectedValue) + ), $message + ); } /** diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index ba2ee9ac..d48df3d4 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -4,9 +4,9 @@ namespace Codeception\Module\Symfony; +use PHPUnit\Framework\Assert; 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 @@ -171,8 +171,8 @@ private function verifyEmailObject(?Email $email, string $function): Email { $email = $email ?: $this->grabLastSentEmail(); $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( + return $email ?? Assert::fail( sprintf($errorMsgTemplate, $function) ); } -} \ No newline at end of file +} diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php index bd9140c0..b4c1a5dd 100644 --- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -5,6 +5,7 @@ namespace Codeception\Module\Symfony; use Codeception\Lib\Connector\Symfony as SymfonyConnector; +use PHPUnit\Framework\Assert; trait ServicesAssertionsTrait { @@ -24,8 +25,10 @@ trait ServicesAssertionsTrait public function grabService(string $serviceId): object { if (!$service = $this->getService($serviceId)) { - $this->fail("Service `{$serviceId}` is required by Codeception, but not loaded by Symfony since you're not using it anywhere in your app.\n - Recommended solution: Set it to `public` in your `config/services_test.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private"); + Assert::fail( + "Service `{$serviceId}` is required by Codeception, but not loaded by Symfony since you're not using it anywhere in your app.\n + Recommended solution: Set it to `public` in your `config/services_test.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private" + ); } return $service; diff --git a/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php index 7c4c385a..5fa91725 100644 --- a/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TranslationAssertionsTrait.php @@ -109,7 +109,7 @@ public function seeDefaultLocaleIs(string $expectedLocale): void * $I->seeFallbackLocalesAre(['es', 'fr']); * ``` * - * @param array $expectedLocales The expected fallback locales + * @param string[] $expectedLocales The expected fallback locales */ public function seeFallbackLocalesAre(array $expectedLocales): void { diff --git a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php index 1bfba3ec..e664932c 100644 --- a/src/Codeception/Module/Symfony/TwigAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/TwigAssertionsTrait.php @@ -79,4 +79,4 @@ protected function grabTwigCollector(string $function): TwigDataCollector { return $this->grabCollector('twig', $function); } -} \ No newline at end of file +} diff --git a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php index ca82e196..508cfa5e 100644 --- a/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ValidatorAssertionsTrait.php @@ -20,7 +20,7 @@ trait ValidatorAssertionsTrait * $I->dontSeeViolatedConstraint($subject, 'propertyName', 'Symfony\Validator\ConstraintClass'); * ``` */ - public function dontSeeViolatedConstraint(mixed $subject, ?string $propertyPath = null, ?string $constraint = null): void + public function dontSeeViolatedConstraint(object $subject, ?string $propertyPath = null, ?string $constraint = null): void { $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint); $this->assertCount(0, $violations, 'Constraint violations found.'); @@ -37,7 +37,7 @@ public function dontSeeViolatedConstraint(mixed $subject, ?string $propertyPath * $I->seeViolatedConstraint($subject, 'propertyName', 'Symfony\Validator\ConstraintClass'); * ``` */ - public function seeViolatedConstraint(mixed $subject, ?string $propertyPath = null, ?string $constraint = null): void + public function seeViolatedConstraint(object $subject, ?string $propertyPath = null, ?string $constraint = null): void { $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint); $this->assertNotCount(0, $violations, 'No constraint violations found.'); @@ -52,7 +52,7 @@ public function seeViolatedConstraint(mixed $subject, ?string $propertyPath = nu * $I->seeViolatedConstraintsCount(2, $subject, 'propertyName'); * ``` */ - public function seeViolatedConstraintsCount(int $expected, mixed $subject, ?string $propertyPath = null, ?string $constraint = null): void + public function seeViolatedConstraintsCount(int $expected, object $subject, ?string $propertyPath = null, ?string $constraint = null): void { $violations = $this->getViolationsForSubject($subject, $propertyPath, $constraint); $this->assertCount($expected, $violations); @@ -66,12 +66,12 @@ public function seeViolatedConstraintsCount(int $expected, mixed $subject, ?stri * $I->seeViolatedConstraintMessage('too short', $user, 'address'); * ``` */ - public function seeViolatedConstraintMessage(string $expected, mixed $subject, string $propertyPath): void + public function seeViolatedConstraintMessage(string $expected, object $subject, string $propertyPath): void { $violations = $this->getViolationsForSubject($subject, $propertyPath); $containsExpected = false; foreach ($violations as $violation) { - if ($violation->getPropertyPath() === $propertyPath && str_contains($violation->getMessage(), $expected)) { + if ($violation->getPropertyPath() === $propertyPath && str_contains((string)$violation->getMessage(), $expected)) { $containsExpected = true; break; } @@ -81,7 +81,7 @@ public function seeViolatedConstraintMessage(string $expected, mixed $subject, s } /** @return ConstraintViolationInterface[] */ - protected function getViolationsForSubject(mixed $subject, ?string $propertyPath = null, ?string $constraint = null): array + protected function getViolationsForSubject(object $subject, ?string $propertyPath = null, ?string $constraint = null): array { $validator = $this->getValidatorService(); $violations = $propertyPath ? $validator->validateProperty($subject, $propertyPath) : $validator->validate($subject); @@ -89,9 +89,9 @@ protected function getViolationsForSubject(mixed $subject, ?string $propertyPath $violations = iterator_to_array($violations); if ($constraint !== null) { - return array_filter( + return (array)array_filter( $violations, - static fn($violation): bool => $violation->getConstraint()::class === $constraint && + static fn(ConstraintViolationInterface $violation): bool => get_class((object)$violation->getConstraint()) === $constraint && ($propertyPath === null || $violation->getPropertyPath() === $propertyPath) ); } From d8a8943d06b303fa9a2feb9064eeeabb5bb36386 Mon Sep 17 00:00:00 2001 From: Thomas Landauer <thomas@landauer.at> Date: Wed, 28 May 2025 20:28:12 +0200 Subject: [PATCH 43/44] Update ServicesAssertionsTrait.php: Adding another hint about private services (#210) --- .../Module/Symfony/ServicesAssertionsTrait.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php index b4c1a5dd..1286e252 100644 --- a/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/ServicesAssertionsTrait.php @@ -11,9 +11,9 @@ trait ServicesAssertionsTrait { /** * Grabs a service from the Symfony dependency injection container (DIC). - * In "test" environment, Symfony uses a special `test.service_container`. + * In the "test" environment, Symfony uses a special `test.service_container`. * See the "[Public Versus Private Services](https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private)" documentation. - * Services that aren't injected somewhere into your app, need to be defined as `public` to be accessible by Codeception. + * Services that aren't injected anywhere in your app, need to be defined as `public` to be accessible by Codeception. * * ```php * <?php @@ -25,10 +25,10 @@ trait ServicesAssertionsTrait public function grabService(string $serviceId): object { if (!$service = $this->getService($serviceId)) { - Assert::fail( - "Service `{$serviceId}` is required by Codeception, but not loaded by Symfony since you're not using it anywhere in your app.\n - Recommended solution: Set it to `public` in your `config/services_test.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private" - ); + Assert::fail("Service `{$serviceId}` is required by Codeception, but not loaded by Symfony. Possible solutions:\n + In your `config/packages/framework.php`/`.yaml`, set `test` to `true` (when in test environment), see https://symfony.com/doc/current/reference/configuration/framework.html#test\n + If you're still getting this message, you're not using that service in your app, so Symfony isn't loading it at all.\n + Solution: Set it to `public` in your `config/services.php`/`.yaml`, see https://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private\n"); } return $service; From c2adb5cdcff816a4190c4bfd5be6acc1a5a9c4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=BCller?= <9016208+Seros@users.noreply.github.com> Date: Fri, 30 May 2025 10:07:26 +0200 Subject: [PATCH 44/44] Replace kernel reboot with actual boot to reset services (#209) * Replace kernel reboot with actual boot to reset services * Ensure congruence with the flow in the Symfony KernelTestCase * Add 6.4wApi branch to CI --------- Co-authored-by: TavoNiievez <ganieves@outlook.com> --- .github/workflows/main.yml | 50 +++++++++++++++++------ src/Codeception/Lib/Connector/Symfony.php | 11 ++++- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43762ad0..c95a2224 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,10 +7,7 @@ jobs: strategy: matrix: php: [8.2, 8.3, 8.4] - symfony: ["5.4.*", "6.4.*", "7.2.*"] - exclude: - - php: 8.1 - symfony: "7.2.*" + symfony: ["5.4.*", "6.4.*", "6.4wApi", "7.2.*"] steps: - name: Checkout code @@ -25,11 +22,26 @@ jobs: coverage: none - name: Set Symfony version reference - run: echo "SF_REF=${MATRIX_SYMFONY%.*}" >> $GITHUB_ENV env: MATRIX_SYMFONY: ${{ matrix.symfony }} + run: | + if [[ "$MATRIX_SYMFONY" == *'*' ]]; then + echo "SF_REF=${MATRIX_SYMFONY%.*}" >> "$GITHUB_ENV" + else + echo "SF_REF=$MATRIX_SYMFONY" >> "$GITHUB_ENV" + fi + + - name: Set Composer Symfony constraint + env: + MATRIX_SYMFONY: ${{ matrix.symfony }} + run: | + if [[ "$MATRIX_SYMFONY" == "6.4wApi" ]]; then + echo "COMP_SYMFONY=6.4.*" >> "$GITHUB_ENV" + else + echo "COMP_SYMFONY=$MATRIX_SYMFONY" >> "$GITHUB_ENV" + fi - - name: Checkout Symfony ${{ env.SF_REF }} Sample + - name: Checkout Symfony ${{ env.SF_REF }} sample uses: actions/checkout@v4 with: repository: Codeception/symfony-module-tests @@ -51,17 +63,24 @@ jobs: run: composer require --dev --no-update "phpunit/phpunit=^10.0" - name: Install dependencies + env: + MATRIX_SYMFONY: ${{ matrix.symfony }} run: | - composer require symfony/finder=${{ matrix.symfony }} --no-update - composer require symfony/yaml=${{ matrix.symfony }} --no-update - composer require symfony/console=${{ matrix.symfony }} --no-update - composer require symfony/event-dispatcher=${{ matrix.symfony }} --no-update - composer require symfony/css-selector=${{ matrix.symfony }} --no-update - composer require symfony/dom-crawler=${{ matrix.symfony }} --no-update - composer require symfony/browser-kit=${{ matrix.symfony }} --no-update + composer require symfony/finder=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/yaml=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/console=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/event-dispatcher=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/css-selector=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/dom-crawler=${{ env.COMP_SYMFONY }} --no-update + composer require symfony/browser-kit=${{ env.COMP_SYMFONY }} --no-update composer require vlucas/phpdotenv --no-update composer require codeception/module-asserts="3.*" --no-update composer require codeception/module-doctrine="3.*" --no-update + + if [[ "$MATRIX_SYMFONY" == "6.4wApi" ]]; then + composer require codeception/module-rest="3.*" --no-update + fi + composer update --prefer-dist --no-progress --no-dev - name: Validate Composer files @@ -84,6 +103,11 @@ jobs: php bin/console doctrine:fixtures:load --quiet working-directory: framework-tests + - name: Generate JWT keypair + if: ${{ matrix.symfony == '6.4wApi' }} + run: php bin/console lexik:jwt:generate-keypair --skip-if-exists + working-directory: framework-tests + - name: Run tests run: | php vendor/bin/codecept build -c framework-tests diff --git a/src/Codeception/Lib/Connector/Symfony.php b/src/Codeception/Lib/Connector/Symfony.php index 44d7595a..019317af 100644 --- a/src/Codeception/Lib/Connector/Symfony.php +++ b/src/Codeception/Lib/Connector/Symfony.php @@ -49,7 +49,7 @@ protected function doRequest(object $request): Response } /** - * Reboot kernel + * Reboots the kernel. * * Services from the list of persistent services * are updated from service container before kernel shutdown @@ -66,7 +66,8 @@ public function rebootKernel(): void } $this->persistDoctrineConnections(); - $this->kernel->reboot(null); + $this->ensureKernelShutdown(); + $this->kernel->boot(); $this->container = $this->getContainer(); foreach ($this->persistentServices as $serviceName => $service) { @@ -82,6 +83,12 @@ public function rebootKernel(): void } } + protected function ensureKernelShutdown(): void + { + $this->kernel->boot(); + $this->kernel->shutdown(); + } + private function getContainer(): ?ContainerInterface { /** @var ContainerInterface $container */ <!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'> <html xmlns='http://www.w3.org/1999/xhtml'> <head> <title>pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy