diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fa6b21a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +Tests export-ignore +.github export-ignore + +.gitattributes export-ignore +.gitignore export-ignore + +phpstan-baseline.neon export-ignore +phpstan.neon export-ignore +phpunit.xml.dist export-ignore +.php-cs-fixer.dist.php export-ignore diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..681aa21 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,155 @@ +name: 'CI' + +on: + push: + branches: + - 'main' + pull_request: + +jobs: + check-composer: + runs-on: 'ubuntu-latest' + steps: + - uses: 'actions/checkout@v3' + + - name: 'Install PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '8.4' + coverage: 'none' + tools: 'composer:v2' + env: + COMPOSER_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Validate composer.json' + run: 'composer validate' + + php-linting: + runs-on: 'ubuntu-latest' + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + - '8.4' + - '8.5' + steps: + - name: 'Checkout' + uses: 'actions/checkout@v3' + + - name: 'Install PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '${{ matrix.php-version }}' + coverage: 'none' + + - name: 'PHP lint' + run: | + find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l + + coding-guideline: + runs-on: 'ubuntu-latest' + needs: + - 'check-composer' + steps: + - uses: 'actions/checkout@v3' + + - name: 'Install PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '8.4' + coverage: 'none' + tools: 'composer:v2' + env: + COMPOSER_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Install dependencies' + run: 'composer install --no-progress --no-interaction --optimize-autoloader' + + - name: 'Coding Guideline' + run: './vendor/bin/php-cs-fixer fix --dry-run --diff' + + tests: + runs-on: 'ubuntu-latest' + needs: + - 'check-composer' + strategy: + matrix: + include: + - php-version: '8.2' + typo3-version: '^13.4' + - php-version: '8.3' + typo3-version: '^13.4' + - php-version: '8.4' + typo3-version: '^13.4' + - php-version: '8.5' + typo3-version: '^13.4' + steps: + - uses: 'actions/checkout@v3' + + - name: 'Install NodeJS' + uses: 'actions/setup-node@v4' + + - name: 'Install node packages' + run: 'npm ci' + + - name: 'Install PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '${{ matrix.php-version }}' + coverage: 'xdebug' + tools: 'composer:v2' + env: + COMPOSER_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Install dependencies with expected TYPO3 version' + run: 'composer require --no-progress --no-interaction --optimize-autoloader "typo3/cms-core:${{ matrix.typo3-version }}"' + + - name: 'PHPUnit Tests' + run: './vendor/bin/phpunit --testdox --display-all-issues --coverage-cobertura coverage/coverage.cobertura.xml' + + - name: 'Code Coverage Report' + uses: 'irongut/CodeCoverageSummary@v1.3.0' + with: + filename: 'coverage/coverage.cobertura.xml' + badge: false + fail_below_min: true + format: 'text' + hide_branch_rate: true + hide_complexity: true + indicators: true + output: 'console' + thresholds: '40 100' + + phpstan: + runs-on: 'ubuntu-latest' + needs: + - 'check-composer' + strategy: + matrix: + include: + - php-version: '8.2' + typo3-version: '^13.4' + - php-version: '8.3' + typo3-version: '^13.4' + - php-version: '8.4' + typo3-version: '^13.4' + - php-version: '8.5' + typo3-version: '^13.4' + steps: + - uses: 'actions/checkout@v3' + + - name: 'Install PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '${{ matrix.php-version }}' + coverage: 'none' + tools: 'composer:v2' + env: + COMPOSER_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Install dependencies with expected TYPO3 version' + run: 'composer require --no-progress --no-interaction --optimize-autoloader "typo3/cms-core:${{ matrix.typo3-version }}"' + + - name: 'PHPStan' + run: './vendor/bin/phpstan' diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..80d6a05 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,25 @@ +setFinder( + (new Finder()) + ->ignoreVCSIgnored(true) + ->in(realpath(__DIR__)) + ) + ->addRules([ + 'fully_qualified_strict_types' => [ + 'import_symbols' => true, + 'leading_backslash_in_global_namespace' => false, + ], + 'global_namespace_import' => false, + 'header_comment' => ['header' => ''], + 'single_line_comment_style' => ['comment_types' => ['hash']], + 'single_line_empty_body' => false, + 'no_trailing_comma_in_singleline_array' => true, + 'php_unit_test_annotation' => ['style' => 'annotation'], + ]); diff --git a/Classes/Domain/Renderer/Command.php b/Classes/Domain/Renderer/Command.php index e992382..6239f4f 100644 --- a/Classes/Domain/Renderer/Command.php +++ b/Classes/Domain/Renderer/Command.php @@ -1,9 +1,10 @@ config = array_merge($this->config, $config); // @phpstan-ignore-line + $this->config = array_merge($this->config, $config); } public function getHtmlFromMjml(string $mjml): string @@ -44,6 +45,10 @@ public function getHtmlFromMjml(string $mjml): string GeneralUtility::unlink_tempfile($temporaryMjmlFileWithPath); + if (is_null($result)) { + return ''; + } + return implode('', $result); } diff --git a/Classes/Domain/Renderer/RendererInterface.php b/Classes/Domain/Renderer/RendererInterface.php index 7be331a..84f7f70 100644 --- a/Classes/Domain/Renderer/RendererInterface.php +++ b/Classes/Domain/Renderer/RendererInterface.php @@ -1,4 +1,5 @@ coreExtensionsToLoad = [ + 'typo3/cms-form', + ]; + + $this->testExtensionsToLoad = [ + 'saccas/mjml', + ]; + + $this->configurationToUseInTestInstance = [ + 'EXTENSIONS' => [ + 'mjml' => [ + 'nodeBinaryPath' => getenv('NODE_BIN_PATH') ?: 'node', + 'mjmlBinaryPath' => 'node_modules/mjml/bin/', + 'mjmlBinary' => 'mjml', + 'mjmlParams' => '-s --config.beautify true --config.minify true', + ], + ], + ]; + + parent::setUp(); + } + + protected function cleanUpGeneratedOutput(string $output): string + { + return preg_replace( + '//Uis', + '', + $output + ) ?? ''; + } +} diff --git a/Tests/Functional/Domain/Renderer/CommandTest.php b/Tests/Functional/Domain/Renderer/CommandTest.php new file mode 100644 index 0000000..220cad7 --- /dev/null +++ b/Tests/Functional/Domain/Renderer/CommandTest.php @@ -0,0 +1,28 @@ +get(Command::class); + + $result = $subject->getHtmlFromMjml( + file_get_contents(__DIR__ . '/CommandTestFixture/Basic.mjml') ?: '' + ); + + self::assertStringEqualsFile( + __DIR__ . '/CommandTestFixture/Expected.html', + $this->cleanUpGeneratedOutput($result), + 'Command renderer did not return expected HTML.' + ); + } +} diff --git a/Tests/Unit/Domain/Renderer/CommandTestFixture/Basic.mjml b/Tests/Functional/Domain/Renderer/CommandTestFixture/Basic.mjml similarity index 100% rename from Tests/Unit/Domain/Renderer/CommandTestFixture/Basic.mjml rename to Tests/Functional/Domain/Renderer/CommandTestFixture/Basic.mjml diff --git a/Tests/Functional/Domain/Renderer/CommandTestFixture/Expected.html b/Tests/Functional/Domain/Renderer/CommandTestFixture/Expected.html new file mode 100644 index 0000000..e1d03dc --- /dev/null +++ b/Tests/Functional/Domain/Renderer/CommandTestFixture/Expected.html @@ -0,0 +1 @@ +
Easy and Quick
Responsive

Discover

\ No newline at end of file diff --git a/Tests/Functional/Mail/MjmlFluidEmailTest.php b/Tests/Functional/Mail/MjmlFluidEmailTest.php new file mode 100644 index 0000000..6de727f --- /dev/null +++ b/Tests/Functional/Mail/MjmlFluidEmailTest.php @@ -0,0 +1,45 @@ +setTemplateRootPaths([ + 'EXT:mjml/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Templates/', + ]); + + $subject = new MjmlFluidEmail($templatePaths); + + $html = $subject->getHtmlBody(); + self::assertIsString($html); + self::assertStringEqualsFile( + __DIR__ . '/MjmlFluidEmailTestFixture/Expected.html', + $this->cleanUpGeneratedOutput($html), + 'Command renderer did not return expected HTML.' + ); + + $text = $subject->getTextBody(); + self::assertIsString($text); + self::assertStringEqualsFile( + __DIR__ . '/MjmlFluidEmailTestFixture/Expected.txt', + $text, + 'Rendering of view did not return expected plain text.' + ); + } +} diff --git a/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.html b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.html new file mode 100644 index 0000000..e1d03dc --- /dev/null +++ b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.html @@ -0,0 +1 @@ +
Easy and Quick
Responsive

Discover

\ No newline at end of file diff --git a/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.txt b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.txt new file mode 100644 index 0000000..2f808b8 --- /dev/null +++ b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Expected.txt @@ -0,0 +1,5 @@ +Easy and Quick + +Responsive + +Discover \ No newline at end of file diff --git a/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Templates/Default/Default.html b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Templates/Default/Default.html new file mode 100644 index 0000000..e8b9e78 --- /dev/null +++ b/Tests/Functional/Mail/MjmlFluidEmailTestFixture/Templates/Default/Default.html @@ -0,0 +1,20 @@ + + + + + + Easy and Quick + + + + Responsive + + + + + Discover + + + + + diff --git a/Tests/Unit/AbstractUnitTestCase.php b/Tests/Unit/AbstractUnitTestCase.php deleted file mode 100644 index 4a4948a..0000000 --- a/Tests/Unit/AbstractUnitTestCase.php +++ /dev/null @@ -1,22 +0,0 @@ -setCacheConfigurations([ - 'extbase_object' => [ - 'backend' => NullBackend::class, - ], - ]); - } -} diff --git a/Tests/Unit/Domain/Renderer/CommandTest.php b/Tests/Unit/Domain/Renderer/CommandTest.php deleted file mode 100644 index 28b1511..0000000 --- a/Tests/Unit/Domain/Renderer/CommandTest.php +++ /dev/null @@ -1,61 +0,0 @@ -getMockBuilder(Package::class) - // ->disableOriginalConstructor() - // ->getMock(); - // $packageMock->expects($this->any()) - // ->method('getPackagePath') - // ->willReturn(dirname(__FILE__, 5) . '/'); - // $packageManagerMock = $this->getMockBuilder(PackageManager::class)->getMock(); - // $packageManagerMock->expects($this->any()) - // ->method('isPackageActive') - // ->with('mjml') - // ->willReturn(true); - // $packageManagerMock->expects($this->any()) - // ->method('getPackage') - // ->with('mjml') - // ->willReturn($packageMock); - // ExtensionManagementUtility::setPackageManager($packageManagerMock); - // - // $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['mjml'] = serialize([ - // 'nodeBinaryPath' => 'node', - // 'mjmlBinaryPath' => 'node_modules/mjml/bin/', - // 'mjmlBinary' => 'mjml', - // 'mjmlParams' => '-s --config.beautify true --config.minify true', - // ]); - // - // $subject = $this->objectManager->get(Command::class); - // $mjml = file_get_contents(__DIR__ . '/CommandTestFixture/Basic.mjml'); - // $html = $subject->getHtmlFromMjml($mjml); - // - // // remove comment rendered by the outputToConsole https://github.com/mjmlio/mjml/blob/50b08513b7a651c234829abfde254f106a62c859/packages/mjml-cli/src/commands/outputToConsole.js#L4 - // $html = preg_replace('//Uis', '', $html); - // - // $this->assertStringEqualsFile( - // __DIR__ . '/CommandTestFixture/Expected.html', - // $html, - // 'Command renderer did not return expected HTML.' - // ); - } -} diff --git a/Tests/Unit/Domain/Renderer/CommandTestFixture/Expected.html b/Tests/Unit/Domain/Renderer/CommandTestFixture/Expected.html deleted file mode 100644 index 5bb42c1..0000000 --- a/Tests/Unit/Domain/Renderer/CommandTestFixture/Expected.html +++ /dev/null @@ -1 +0,0 @@ -
Easy and Quick
Responsive

Discover

\ No newline at end of file diff --git a/Tests/Unit/View/MjmlBasedViewTest.php b/Tests/Unit/View/MjmlBasedViewTest.php deleted file mode 100644 index f0a73a0..0000000 --- a/Tests/Unit/View/MjmlBasedViewTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - Easy and Quick - - - - Responsive - - - - - Discover - - - - -'; - - /** - * @test - */ - public function viewCallsRendererAndReturnsRenderedHtml(): void - { - // TODO v13 Upgrade test - // $expectedHtml = '

Simple HTML

'; - // $rendererMock = $this->getMockBuilder(RendererInterface::class)->getMock(); - // $rendererMock->expects($this->once()) - // ->method('getHtmlFromMjml') - // ->with(static::EXAMPLE_MJML_TEMPLATE) - // ->willReturn($expectedHtml); - // - // $subject = new MjmlBasedView(null, $rendererMock); - // $subject->setTemplateSource(static::EXAMPLE_MJML_TEMPLATE); - // $result = $subject->render(); - // - // $this->assertSame( - // $expectedHtml, - // $result, - // 'Rendering of view did not return expected HTML.' - // ); - } -} diff --git a/composer.json b/composer.json index 8064f52..076f827 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,12 @@ "description": "Mjml view using mjml over npm", "type": "typo3-cms-extension", "homepage": "https://mjml.io", + "keywords": [ + "TYPO3", + "extension", + "email", + "mjml" + ], "license": ["GPL-2.0-or-later"], "autoload": { "psr-4": { @@ -16,37 +22,34 @@ }, "require": { "php": "^8.2", + "html2text/html2text": "^4.3", "typo3/cms-core": "^13.4", - "typo3/cms-form": "^13.4", - "html2text/html2text": "^4.3" - }, + "typo3/cms-form": "^13.4" + }, "require-dev": { - "squizlabs/php_codesniffer": "^3.2.0", - "typo3/testing-framework": "^1.2.2 || ^6.16" + "phpstan/extension-installer": "^1.4", + "saschaegerer/phpstan-typo3": "^2.1", + "typo3/coding-standards": "^0.8.0", + "typo3/testing-framework": "^9.3" }, "scripts": { - "lint": [ - "! find Classes -type f -name \"*.php\" -exec php -d error_reporting=32767 -l {} \\; 2>&1 >&- | grep \"^\"", - "! find Tests -type f -name \"*.php\" -exec php -d error_reporting=32767 -l {} \\; 2>&1 >&- | grep \"^\"" - ], - "cgl": [ - "./vendor/bin/phpcs" - ], - "test": [ - "TYPO3_PATH_ROOT=web ./vendor/bin/phpunit" - ], "post-install-cmd": [ "npm install" ] }, - "replace": { - "typo3-ter/mjml": "self.version" - }, "extra": { "typo3/cms": { "extension-key": "mjml", - "cms-package-dir": "{$vendor-dir}/typo3/cms", - "web-dir": "web" + "web-dir": ".Build/web" + } + }, + "config": { + "sort-packages": true, + "lock": false, + "allow-plugins": { + "phpstan/extension-installer": true, + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..86dbb16 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,37 @@ +parameters: + ignoreErrors: + - + message: '#^Cannot access offset ''MAIL'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php + + - + message: '#^Cannot access offset ''layoutRootPaths'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php + + - + message: '#^Cannot access offset ''partialRootPaths'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php + + - + message: '#^Cannot access offset ''templateRootPaths'' on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php + + - + message: '#^Parameter \#1 \$array of function array_replace_recursive expects array, mixed given\.$#' + identifier: argument.type + count: 3 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php + + - + message: '#^Parameter \#1 \$templateName of method TYPO3\\CMS\\Core\\Mail\\FluidEmail\:\:setTemplate\(\) expects string, mixed given\.$#' + identifier: argument.type + count: 1 + path: Classes/Domain/Finishers/MjmlEmailFinisher.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5d5d55f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +includes: + - 'phpstan-baseline.neon' +parameters: + level: 'max' + paths: + - 'Classes' + - 'Tests' + reportUnmatchedIgnoredErrors: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f8dad0b..d9cc5f0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,28 +1,31 @@ + + cacheDirectory=".phpunit.cache" + backupStaticProperties="false" + beStrictAboutCoverageMetadata="true" + requireCoverageMetadata="true"> - - ./Tests/Unit + + ./Tests/Functional/ - - + + ./Classes - - + + + + + +