diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ab42b8c5..85b0d49d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,24 +1,20 @@
name: CI
-
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
-
strategy:
matrix:
- php: [8.0, 8.1, 8.2]
- symfony: ["4.4.*", "5.4.*", "6.0.*", "6.1.*", "6.2.*"]
+ php: [8.1, 8.2, 8.3, 8.4]
+ symfony: ["5.4.*", "6.4.*", "7.2.*"]
exclude:
- - php: 8.0
- symfony: "6.1.*"
- - php: 8.0
- symfony: "6.2.*"
+ - php: 8.1
+ symfony: "7.2.*"
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -28,56 +24,31 @@ 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
- 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
- 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: Set Symfony version reference
+ run: echo "SF_REF=${MATRIX_SYMFONY%.*}" >> $GITHUB_ENV
+ env:
+ MATRIX_SYMFONY: ${{ matrix.symfony }}
- - name: Checkout Symfony 6.2 Sample
- if: "matrix.symfony == '6.2.*'"
- uses: actions/checkout@v2
+ - name: Checkout Symfony ${{ env.SF_REF }} Sample
+ uses: actions/checkout@v4
with:
repository: Codeception/symfony-module-tests
path: framework-tests
- ref: "6.2"
+ 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
- uses: actions/cache@v2.1.3
+ - 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"
- name: Install dependencies
run: |
@@ -90,26 +61,30 @@ 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
+ - name: Validate Composer files
+ run: composer validate --strict
+ working-directory: 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-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
- - name: Prepare the test environment
+ - name: Setup Database
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
+ - name: Run tests
run: |
php vendor/bin/codecept build -c framework-tests
php vendor/bin/codecept run Functional -c framework-tests
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
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 1e03edcd..747a5941 100644
--- a/composer.json
+++ b/composer.json
@@ -5,6 +5,7 @@
"type": "library",
"keywords": [
"codeception",
+ "functional testing",
"symfony"
],
"authors": [
@@ -18,22 +19,43 @@
],
"homepage": "https://codeception.com/",
"require": {
- "php": "^8.0",
+ "php": "^8.1",
"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",
+ "codeception/module-doctrine": "^3.1",
"doctrine/orm": "^2.10",
- "symfony/form": "^4.4 | ^5.0 | ^6.0",
- "symfony/framework-bundle": "^4.4 | ^5.0 | ^6.0",
- "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0",
- "symfony/mailer": "^4.4 | ^5.0 | ^6.0",
- "symfony/routing": "^4.4 | ^5.0 | ^6.0",
- "symfony/security-bundle": "^4.4 | ^5.0 | ^6.0",
- "symfony/twig-bundle": "^4.4 | ^5.0 | ^6.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/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",
"vlucas/phpdotenv": "^4.2 | ^5.4"
},
"suggest": {
diff --git a/readme.md b/readme.md
index c832d43a..c5bbcb98 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`, `7.2.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..44d7595a 100644
--- a/src/Codeception/Lib/Connector/Symfony.php
+++ b/src/Codeception/Lib/Connector/Symfony.php
@@ -20,36 +20,22 @@
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
- */
- 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) {
@@ -81,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());
}
}
@@ -102,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 fc8044ca..3ac2bc79 100644
--- a/src/Codeception/Module/Symfony.php
+++ b/src/Codeception/Module/Symfony.php
@@ -13,8 +13,11 @@
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\LoggerAssertionsTrait;
use Codeception\Module\Symfony\MailerAssertionsTrait;
use Codeception\Module\Symfony\MimeAssertionsTrait;
use Codeception\Module\Symfony\ParameterAssertionsTrait;
@@ -23,15 +26,19 @@
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;
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;
@@ -39,12 +46,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_merge;
-use function array_search;
use function array_unique;
use function class_exists;
use function codecept_root_dir;
@@ -53,7 +57,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;
@@ -63,7 +66,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.
@@ -74,19 +77,20 @@
*
* ## 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
- * * 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 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)
+ * * `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) (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.
+ * * `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:
@@ -119,14 +123,14 @@
* enabled:
* - Symfony:
* part: services
- * - Doctrine2:
+ * - Doctrine:
* depends: Symfony
* - WebDriver:
* url: http://example.com
* 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.
*
*/
@@ -135,8 +139,11 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule
use BrowserAssertionsTrait;
use ConsoleAssertionsTrait;
use DoctrineAssertionsTrait;
+ use DomCrawlerAssertionsTrait;
use EventsAssertionsTrait;
use FormAssertionsTrait;
+ use HttpClientAssertionsTrait;
+ use LoggerAssertionsTrait;
use MailerAssertionsTrait;
use MimeAssertionsTrait;
use ParameterAssertionsTrait;
@@ -144,8 +151,10 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule
use SecurityAssertionsTrait;
use ServicesAssertionsTrait;
use SessionAssertionsTrait;
+ use TranslationAssertionsTrait;
use TimeAssertionsTrait;
use TwigAssertionsTrait;
+ use ValidatorAssertionsTrait;
public Kernel $kernel;
@@ -166,46 +175,38 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule
'em_service' => 'doctrine.orm.entity_manager',
'rebootable_client' => true,
'authenticator' => false,
+ 'bootstrap' => false,
'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']) {
+ $this->bootstrapEnvironment();
+ }
$this->kernel->boot();
-
- if ($this->config['cache_router'] === true) {
+ if ($this->config['cache_router']) {
$this->persistPermanentService('router');
}
}
@@ -227,7 +228,6 @@ public function _after(TestInterface $test): void
foreach (array_keys($this->permanentServices) as $serviceName) {
$this->permanentServices[$serviceName] = $this->grabService($serviceName);
}
-
parent::_after($test);
}
@@ -250,40 +250,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
@@ -309,9 +293,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,
@@ -320,42 +305,33 @@ protected function getKernelClass(): string
);
}
- $this->requireAdditionalAutoloader();
-
- $filesRealPath = array_map(function ($file) {
+ $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)) {
+ if (in_array($reflectionClass->getFileName(), $filesRealPath, true)) {
return $kernelClass;
}
-
- throw new ModuleRequireException(self::class, "Kernel class was not found in {$file}.");
}
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.'
);
}
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);
- } catch (BadMethodCallException $e) {
+ return $profiler?->loadProfileFromResponse($this->getClient()->getResponse());
+ } catch (BadMethodCallException) {
$this->fail('You must perform a request before using this method.');
} catch (Exception $e) {
$this->fail($e->getMessage());
@@ -367,22 +343,14 @@ 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
{
- 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);
@@ -391,49 +359,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);
- }
}
/**
@@ -442,15 +384,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();
}
}
}
@@ -458,6 +399,54 @@ 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 {
+ 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');
+ }
+ }
+
+ 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.
diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
index cabd34f2..fbd8a075 100644
--- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
+++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php
@@ -4,14 +4,273 @@
namespace Codeception\Module\Symfony;
+use PHPUnit\Framework\Constraint\Constraint;
+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
{
/**
- * Reboot client's kernel.
- * Can be used to manually reboot kernel when 'rebootable_client' => false
+ * Asserts that the given cookie in the test client is set to the expected value.
+ *
+ * ```php
+ * assertBrowserCookieValueSame('cookie_name', 'expected_value');
+ * ```
+ */
+ public function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForClient(new BrowserHasCookie($name, $path, $domain), $message);
+ $this->assertThatForClient(new BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain), $message);
+ }
+
+ /**
+ * Asserts that the test client has the specified cookie set.
+ * This indicates that the cookie was set by any response during the test.
+ *
+ * ```
+ * assertBrowserHasCookie('cookie_name');
+ * ```
+ */
+ 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 specified cookie set.
+ * This indicates that the cookie was not set by any response during the test.
+ *
+ * ```php
+ * assertBrowserNotHasCookie('cookie_name');
+ * ```
+ */
+ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForClient(new LogicalNot(new BrowserHasCookie($name, $path, $domain)), $message);
+ }
+
+ /**
+ * Asserts that the specified request attribute matches the expected value.
+ *
+ * ```php
+ * assertRequestAttributeValueSame('attribute_name', 'expected_value');
+ * ```
+ */
+ public function assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThat($this->getClient()->getRequest(), new RequestAttributeValueSame($name, $expectedValue), $message);
+ }
+
+ /**
+ * Asserts that the specified response cookie is present and matches the expected value.
+ *
+ * ```php
+ * assertResponseCookieValueSame('cookie_name', 'expected_value');
+ * ```
+ */
+ public function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHasCookie($name, $path, $domain), $message);
+ $this->assertThatForResponse(new ResponseCookieValueSame($name, $expectedValue, $path, $domain), $message);
+ }
+
+ /**
+ * Asserts that the response format matches the expected format. This checks the format returned by the `Response::getFormat()` method.
+ *
+ * ```php
+ * assertResponseFormatSame('json');
+ * ```
+ */
+ public function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseFormatSame($this->getClient()->getRequest(), $expectedFormat), $message);
+ }
+
+ /**
+ * Asserts that the specified cookie is present in the response. Optionally, it can check for a specific cookie path or domain.
+ *
+ * ```php
+ * assertResponseHasCookie('cookie_name');
+ * ```
+ */
+ public function assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHasCookie($name, $path, $domain), $message);
+ }
+
+ /**
+ * Asserts that the specified header is available in the response.
+ * For example, use `assertResponseHasHeader('content-type');`.
+ *
+ * ```php
+ * assertResponseHasHeader('content-type');
+ * ```
+ */
+ public function assertResponseHasHeader(string $headerName, string $message = ''): void
+ {
+ $this->assertThatForResponse(new ResponseHasHeader($headerName), $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
+ * assertResponseHeaderNotSame('content-type', 'application/json');
+ * ```
+ */
+ public function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHeaderSame($headerName, $expectedValue)), $message);
+ }
+
+ /**
+ * Asserts that the specified header contains the expected value in the response.
+ * For example, use `assertResponseHeaderSame('content-type', 'application/octet-stream');`.
+ *
+ * ```php
+ * assertResponseHeaderSame('content-type', 'application/json');
+ * ```
+ */
+ 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 code is in the 2xx range).
+ *
+ * ```php
+ * assertResponseIsSuccessful();
+ * ```
+ */
+ public function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseIsSuccessful($verbose), $message);
+ }
+
+ /**
+ * Asserts that the response is unprocessable (HTTP status code is 422).
+ *
+ * ```php
+ * assertResponseIsUnprocessable();
+ * ```
+ */
+ public function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseIsUnprocessable($verbose), $message);
+ }
+
+ /**
+ * Asserts that the specified cookie is not present in the response. Optionally, it can check for a specific cookie path or domain.
+ *
+ * ```php
+ * assertResponseNotHasCookie('cookie_name');
+ * ```
+ */
+ public function assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHasCookie($name, $path, $domain)), $message);
+ }
+
+ /**
+ * Asserts that the specified header is not available in the response.
+ *
+ * ```php
+ * assertResponseNotHasHeader('content-type');
+ * ```
+ */
+ public function assertResponseNotHasHeader(string $headerName, string $message = ''): void
+ {
+ $this->assertThatForResponse(new LogicalNot(new ResponseHasHeader($headerName)), $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
+ * assertResponseRedirects('/login', 302);
+ * ```
+ */
+ public function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true): void
+ {
+ $this->assertThatForResponse(new ResponseIsRedirected($verbose), $message);
+
+ if ($expectedLocation) {
+ $constraint = class_exists(ResponseHeaderLocationSame::class)
+ ? new ResponseHeaderLocationSame($this->getClient()->getRequest(), $expectedLocation)
+ : new ResponseHeaderSame('Location', $expectedLocation);
+ $this->assertThatForResponse($constraint, $message);
+ }
+
+ if ($expectedCode) {
+ $this->assertThatForResponse(new ResponseStatusCodeSame($expectedCode), $message);
+ }
+ }
+
+ /**
+ * Asserts that the response status code matches the expected code.
+ *
+ * ```php
+ * assertResponseStatusCodeSame(200);
+ * ```
+ */
+ 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.
+ *
+ * ```php
+ * assertRouteSame('profile', ['id' => 123]);
+ * ```
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Reboots the client's kernel.
+ * Can be used to manually reboot the kernel when 'rebootable_client' is set to false.
*
* ```php
* 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
+ public function seePageIsAvailable(?string $url = null): void
{
if ($url !== null) {
$this->amOnPage($url);
$this->seeInCurrentUrl($url);
}
- $this->assertThat($this->getClient()->getResponse(), new ResponseIsSuccessful());
+ $this->assertResponseIsSuccessful();
}
/**
- * Goes to a page and check that it redirects to another.
+ * Navigates to a page and verifies that it redirects to another page.
*
* ```php
* seePageRedirectsTo('/admin', '/login');
* ```
- *
- * @param string $page
- * @param string $redirectsTo
*/
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
* ` (you cannot use an array as selector here)
- * @param string[] $fields
+ * @param string $name The `name` attribute of the `