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..747a5941 100644 --- a/composer.json +++ b/composer.json @@ -28,33 +28,34 @@ "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/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 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 diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index 3f7917bb..3ac2bc79 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; @@ -25,6 +26,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; @@ -141,6 +143,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use EventsAssertionsTrait; use FormAssertionsTrait; use HttpClientAssertionsTrait; + use LoggerAssertionsTrait; use MailerAssertionsTrait; use MimeAssertionsTrait; use ParameterAssertionsTrait; @@ -148,6 +151,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use SecurityAssertionsTrait; use ServicesAssertionsTrait; use SessionAssertionsTrait; + use TranslationAssertionsTrait; use TimeAssertionsTrait; use TwigAssertionsTrait; use ValidatorAssertionsTrait; @@ -317,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.' ); } 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 + * 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. + * + * ``` + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 + * 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 `` 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/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); + } +} diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index df2fd0f1..5a31e6d8 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,12 @@ 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 + * $I->dontSeeEmailIsSent(); + * ``` */ public function dontSeeEmailIsSent(): void { @@ -60,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 @@ -82,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 @@ -100,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 @@ -114,18 +146,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."); } } 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); + } +}