From 6662fc7d019281e1f40886c201b5969edd0ab102 Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Thu, 27 Aug 2026 18:42:30 +0200 Subject: [PATCH 1/2] Test performance --- .github/workflows/test.yml | 17 +- Changelog.md | 4 + MultisiteLanguageSwitcher.php | 5 +- composer.json | 2 +- composer.lock | 130 ++++++--- readme.txt | 12 +- src/msls-widget-block/block.json | 2 +- tests/phpunit/Admin/TestAdmin.php | 165 +++++------ tests/phpunit/Admin/TestIcon.php | 17 +- tests/phpunit/Admin/TestMetaBox.php | 30 +- .../Admin/TranslationPicker/TestPage.php | 69 +++-- tests/phpunit/Blog/TestBlog.php | 20 +- tests/phpunit/Blog/TestCollection.php | 90 ++++-- tests/phpunit/Compat/TestAliases.php | 61 ++-- tests/phpunit/Component/Icon/TestIconPng.php | 21 +- tests/phpunit/Component/Icon/TestIconSvg.php | 21 +- .../TestAttachmentPathFinder.php | 45 +-- .../ContentImport/TestImportCoordinates.php | 22 +- .../ContentImport/TestImportLogger.php | 16 +- tests/phpunit/Frontend/Map/TestHrefLang.php | 27 +- tests/phpunit/Frontend/TestContentFilter.php | 26 +- tests/phpunit/Frontend/TestOutput.php | 271 ++++++++---------- tests/phpunit/Frontend/TestWidget.php | 16 +- tests/phpunit/Options/Query/TestQuery.php | 97 +++---- tests/phpunit/Options/TestOptions.php | 70 +++-- tests/phpunit/TestDeprecated.php | 56 ++-- 26 files changed, 729 insertions(+), 583 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4e50dffe..1b774e780 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,4 +28,19 @@ jobs: uses: codecov/codecov-action@v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - file: tests/coverage.xml \ No newline at end of file + file: tests/coverage.xml + php74-compat: + name: PHP 7.4 (production dependencies) + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v7 + - name: Setup PHP 7.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer:v2 + - name: Install production dependencies + run: composer install --no-dev --prefer-dist --no-progress + - name: Lint the shipped PHP files + run: git ls-files '*.php' | grep -vE '^(tests|bin)/' | xargs -n1 -P4 php -l diff --git a/Changelog.md b/Changelog.md index 44fd2a470..1780176bc 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +## 3.0.3 +* Fix: the plugin required PHP-DI 7, which needs PHP 8.0, while MSLS itself declares PHP 7.4 as its minimum. PHP-DI is back on the 6.4 series, so an installation on PHP 7.4 can resolve the dependencies again. +* Internal: declare `Requires PHP: 7.4` in the plugin header so WordPress blocks activation on older PHP versions, and check the production dependencies against PHP 7.4 in CI. + ## 3.0.2 * Fix: opening the settings page on a blog with more users than the "Reference user" dropdown shows raised a PHP notice, which landed in the error log and in services such as Sentry. The limit is now reported as a hint below the dropdown, where it belongs. * Internal: detect the truncated user list from the query itself instead of calling `count_users()`, which drops a costly query from the settings page on blogs with many users. diff --git a/MultisiteLanguageSwitcher.php b/MultisiteLanguageSwitcher.php index e02612992..3f9d93c7d 100644 --- a/MultisiteLanguageSwitcher.php +++ b/MultisiteLanguageSwitcher.php @@ -3,9 +3,10 @@ * Multisite Language Switcher Plugin * * Plugin Name: Multisite Language Switcher - * Version: 3.0.2 + * Version: 3.0.3 * Plugin URI: http://msls.co/ * Description: A simple but powerful plugin that will help you to manage the relations of your contents in a multilingual multisite-installation. + * Requires PHP: 7.4 * Author: Dennis Ploetner * Author URI: http://lloc.de/ * Text Domain: multisite-language-switcher @@ -42,7 +43,7 @@ * @author Dennis Ploetner */ if ( ! defined( 'MSLS_PLUGIN_VERSION' ) ) { - define( 'MSLS_PLUGIN_VERSION', '3.0.2' ); + define( 'MSLS_PLUGIN_VERSION', '3.0.3' ); define( 'MSLS_PLUGIN_PATH', plugin_basename( __FILE__ ) ); define( 'MSLS_PLUGIN__FILE__', __FILE__ ); diff --git a/composer.json b/composer.json index 0764165ca..de546b968 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require": { "php": ">=7.4", "composer/installers": "~2.3.0", - "php-di/php-di": "^7.0", + "php-di/php-di": "^6.4", "ext-json": "*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 22264902e..08012b170 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "61545832b6ce6ec35c622a459b725e16", + "content-hash": "75744df76ef89bcde4eec6f0c7860707", "packages": [ { "name": "composer/installers", @@ -154,32 +154,32 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.15", + "version": "v1.3.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/4f48ade902b94323ca3be7646db16209ec76be3d", + "reference": "4f48ade902b94323ca3be7646db16209ec76be3d", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^7.3|^8.0" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0|^13.0", - "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0|^4.0", - "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -211,7 +211,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-07-21T16:49:22+00:00" + "time": "2024-11-14T18:34:49+00:00" }, { "name": "php-di/invoker", @@ -270,36 +270,39 @@ }, { "name": "php-di/php-di", - "version": "7.1.1", + "version": "6.4.0", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + "reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", - "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/ae0f1b3b03d8b29dff81747063cbfd6276246cc4", + "reference": "ae0f1b3b03d8b29dff81747063cbfd6276246cc4", "shasum": "" }, "require": { - "laravel/serializable-closure": "^1.0 || ^2.0", - "php": ">=8.0", + "laravel/serializable-closure": "^1.0", + "php": ">=7.4.0", "php-di/invoker": "^2.0", - "psr/container": "^1.1 || ^2.0" + "php-di/phpdoc-reader": "^2.0.1", + "psr/container": "^1.0" }, "provide": { "psr/container-implementation": "^1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "friendsofphp/proxy-manager-lts": "^1", - "mnapoli/phpunit-easymock": "^1.3", - "phpunit/phpunit": "^9.6 || ^10 || ^11", - "vimeo/psalm": "^5|^6" + "doctrine/annotations": "~1.10", + "friendsofphp/php-cs-fixer": "^2.4", + "mnapoli/phpunit-easymock": "^1.2", + "ocramius/proxy-manager": "^2.11.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9.5" }, "suggest": { - "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", + "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" }, "type": "library", "autoload": { @@ -327,7 +330,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + "source": "https://github.com/PHP-DI/PHP-DI/tree/6.4.0" }, "funding": [ { @@ -339,31 +342,68 @@ "type": "tidelift" } ], - "time": "2025-08-16T11:10:48+00:00" + "time": "2022-04-09T16:46:38+00:00" + }, + { + "name": "php-di/phpdoc-reader", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PhpDocReader.git", + "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/66daff34cbd2627740ffec9469ffbac9f8c8185c", + "reference": "66daff34cbd2627740ffec9469ffbac9f8c8185c", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpDocReader\\": "src/PhpDocReader" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", + "keywords": [ + "phpdoc", + "reflection" + ], + "support": { + "issues": "https://github.com/PHP-DI/PhpDocReader/issues", + "source": "https://github.com/PHP-DI/PhpDocReader/tree/2.2.1" + }, + "time": "2020-10-12T12:39:22+00:00" }, { "name": "psr/container", - "version": "2.0.2", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -390,9 +430,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-11-05T16:50:12+00:00" } ], "packages-dev": [ @@ -2983,16 +3023,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.14", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "13b38720174286f55d1761152b575a8d1436fc25" + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25", - "reference": "13b38720174286f55d1761152b575a8d1436fc25", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", "shasum": "" }, "require": { @@ -3027,7 +3067,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.14" + "source": "https://github.com/symfony/finder/tree/v7.4.17" }, "funding": [ { @@ -3047,7 +3087,7 @@ "type": "tidelift" } ], - "time": "2026-06-27T08:31:18+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "szepeviktor/phpstan-wordpress", diff --git a/readme.txt b/readme.txt index ae2b33d11..ab82daf41 100644 --- a/readme.txt +++ b/readme.txt @@ -6,7 +6,7 @@ Tags: multilingual, multisite, language, switcher, localization Requires at least: 6.1 Tested up to: 7.1 Requires PHP: 7.4 -Stable tag: 3.0.2 +Stable tag: 3.0.3 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -93,6 +93,10 @@ Please visit the [MSLS website](https://msls.co/) or use the [WordPress support == Changelog == += 3.0.3 = + +* Fixed: on PHP 7.4 the plugin's dependencies could not be installed, because one of them required PHP 8. MSLS supports PHP 7.4 again, as documented. + = 3.0.2 = * Fixed: with more than 100 users on a blog, opening the settings page raised a PHP notice that ended up in the error log (and in services such as Sentry). The truncated user list is now reported as a hint below the "Reference user" dropdown instead. @@ -134,6 +138,12 @@ The full history is kept in the separate [Changelog](https://github.com/lloc/Mul == Upgrade Notice == += 3.0.3 = + +Maintenance release for PHP 7.4 installations. One of the plugin's dependencies required +PHP 8, which broke the installation on the PHP version MSLS documents as its minimum. No +changes to the plugin's behaviour. + = 3.0.2 = Bugfix release. The settings page no longer logs a PHP notice when the blog has more users diff --git a/src/msls-widget-block/block.json b/src/msls-widget-block/block.json index 19b4c0b38..bbdfe9ef4 100644 --- a/src/msls-widget-block/block.json +++ b/src/msls-widget-block/block.json @@ -5,7 +5,7 @@ "icon": "translation", "category": "widgets", "name": "lloc/msls-widget-block", - "version": "3.0.2", + "version": "3.0.3", "description": "Review the settings for the Multisite Language Switcher plugin, as the block utilizes the API function `msls_the_switcher()` for its output.", "example": {}, "supports": { diff --git a/tests/phpunit/Admin/TestAdmin.php b/tests/phpunit/Admin/TestAdmin.php index 11cf90cb3..1e539b767 100644 --- a/tests/phpunit/Admin/TestAdmin.php +++ b/tests/phpunit/Admin/TestAdmin.php @@ -8,6 +8,7 @@ use lloc\Msls\Blog\Collection; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestAdmin extends MslsUnitTestCase { @@ -57,17 +58,20 @@ private function AdminFactory( array $users = array() ): Admin { return new Admin( $options, $collection ); } - public static function has_problems_data(): array { + /** + * @return array, bool, string}> + */ + public static function has_problems_provider(): array { + $warning = '/^

.*$/'; + return array( - array( array( 'de_DE', 'it_IT' ), false, '/^$/' ), - array( array( 'de_DE' ), false, '/^

.*$/' ), - array( array(), true, '/^

.*$/' ), + 'two languages, options filled' => array( array( 'de_DE', 'it_IT' ), false, '/^$/' ), + 'only one language' => array( array( 'de_DE' ), false, $warning ), + 'no languages, options empty' => array( array(), true, $warning ), ); } - /** - * @dataProvider has_problems_data - */ + #[DataProvider( 'has_problems_provider' )] public function test_has_problems( array $languages, bool $is_empty, string $regex ): void { Functions\when( 'get_option' )->justReturn( array() ); Functions\when( 'get_current_blog_id' )->justReturn( 1 ); @@ -189,96 +193,68 @@ function ( array $list ): array { $obj->reference_user(); } - public function test_activate_autocomplete(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->activate_autocomplete(); - } - - public function test_sort_by_description(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->sort_by_description(); - } - - - public function test_exclude_current_blog(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->exclude_current_blog(); - } - - function test_only_with_translation(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->only_with_translation(); - } - - function test_output_current_blog(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->output_current_blog(); - } - - function test_description(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - '' - ); - $obj->description(); - } - - function test_before_output(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - '' - ); - $obj->before_output(); - } - - function test_after_output(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - '' + /** + * Every one of these renders a single settings field and writes nothing else. + * + * @return array + */ + public static function settings_field_provider(): array { + return array( + 'activate_autocomplete' => array( + 'activate_autocomplete', + ' ', + ), + 'sort_by_description' => array( + 'sort_by_description', + ' ', + ), + 'exclude_current_blog' => array( + 'exclude_current_blog', + ' ', + ), + 'only_with_translation' => array( + 'only_with_translation', + ' ', + ), + 'output_current_blog' => array( + 'output_current_blog', + ' ', + ), + 'description' => array( + 'description', + '', + ), + 'before_output' => array( + 'before_output', + '', + ), + 'after_output' => array( + 'after_output', + '', + ), + 'before_item' => array( + 'before_item', + '', + ), + 'after_item' => array( + 'after_item', + '', + ), + 'content_filter' => array( + 'content_filter', + ' ', + ), ); - $obj->after_output(); } - function test_before_item(): void { + #[DataProvider( 'settings_field_provider' )] + public function test_settings_field_renders( string $method, string $expected ): void { $obj = $this->AdminFactory(); - $this->expectOutputString( - '' - ); - $obj->before_item(); + $this->expectOutputString( $expected ); + $obj->{$method}(); } - function test_after_item(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - '' - ); - $obj->after_item(); - } function test_rewrite_tizio(): void { $obj = $this->AdminFactory(); @@ -322,15 +298,6 @@ function test_rewrite_pallino(): void { $obj->rewrite_pallino( 'pallino' ); } - function test_content_filter(): void { - $obj = $this->AdminFactory(); - - $this->expectOutputString( - ' ' - ); - $obj->content_filter(); - } - function test_content_priority(): void { $obj = $this->AdminFactory(); diff --git a/tests/phpunit/Admin/TestIcon.php b/tests/phpunit/Admin/TestIcon.php index d28aeb852..ad454a142 100644 --- a/tests/phpunit/Admin/TestIcon.php +++ b/tests/phpunit/Admin/TestIcon.php @@ -5,6 +5,7 @@ use Brain\Monkey\Functions; use lloc\Msls\Admin\Icon; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestIcon extends MslsUnitTestCase { @@ -178,17 +179,21 @@ public function test_set_icon_type(): void { $this->assertInstanceOf( Icon::class, $obj->set_icon_type( 'flag' ) ); } + /** + * A null language means none was set, which is what the empty expectation is about. + * + * @return array + */ public static function icon_type_provider(): array { return array( - array( 'flag', 'de_DE', 'de_DE' ), - array( 'label', 'it_IT', 'itIT' ), + 'flag' => array( 'flag', 'de_DE', 'de_DE' ), + 'label' => array( 'label', 'it_IT', 'itIT' ), + 'flag without a language' => array( 'flag', null, '' ), ); } - /** - * @dataProvider icon_type_provider - */ - public function test_get_icon_flag( ?string $icon_type, ?string $language, string $expected ): void { + #[DataProvider( 'icon_type_provider' )] + public function test_get_icon( ?string $icon_type, ?string $language, string $expected ): void { Functions\expect( 'plugin_dir_path' )->andReturn( dirname( __DIR__, 3 ) . '/' ); $obj = new Icon( 'post' ); diff --git a/tests/phpunit/Admin/TestMetaBox.php b/tests/phpunit/Admin/TestMetaBox.php index 98e2b9e26..74d06bda1 100644 --- a/tests/phpunit/Admin/TestMetaBox.php +++ b/tests/phpunit/Admin/TestMetaBox.php @@ -14,6 +14,7 @@ use lloc\Msls\Options\Post\Post; use lloc\Msls\Request\Fields; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestMetaBox extends MslsUnitTestCase { @@ -159,17 +160,18 @@ public function test_render_options(): void { $this->assertEquals( '', $test->render_options( 'post', 42 ) ); } - public static function add_data_provider(): array { + /** + * @return array + */ + public static function add_provider(): array { return array( - array( array( 'post', 'page' ), true, true ), - array( array( 'book' ), false, false ), + 'content import and autocomplete on' => array( true, true ), + 'content import and autocomplete off' => array( false, false ), ); } - /** - * @dataProvider add_data_provider - */ - public function test_add( $post_type, $content_import, $autocomplete ) { + #[DataProvider( 'add_provider' )] + public function test_add( $content_import, $autocomplete ) { $options = \Mockery::mock( Options::class ); $options->activate_content_import = $content_import; $options->activate_autocomplete = $autocomplete; @@ -257,16 +259,20 @@ public function test_render_select_hierarchical(): void { $this->MetaBoxFactory()->render_select(); } + /** + * The numeric columns are Mockery invocation counts for the WordPress functions the + * rendering calls, not domain data. + * + * @return array, int, int, int, int, int, string}> + */ public static function render_input_provider(): array { return array( - array( array( 'de_DE' => 42 ), 1, 1, 0, 2, 0, '

' ), - array( array( 'en_US' => 17 ), 0, 3, 2, 0, 2, '' ), + 'existing translation renders an edit link' => array( array( 'de_DE' => 42 ), 1, 1, 0, 2, 0, '' ), + 'missing translation renders a create link' => array( array( 'en_US' => 17 ), 0, 3, 2, 0, 2, '' ), ); } - /** - * @dataProvider render_input_provider - */ + #[DataProvider( 'render_input_provider' )] public function test_render_input( $option, $the_title_times, $current_blog_id_times, $admin_url_times, $edit_post_link_times, $add_query_arg_times, $expected ) { global $post; diff --git a/tests/phpunit/Admin/TranslationPicker/TestPage.php b/tests/phpunit/Admin/TranslationPicker/TestPage.php index e80ea942d..e84e22208 100644 --- a/tests/phpunit/Admin/TranslationPicker/TestPage.php +++ b/tests/phpunit/Admin/TranslationPicker/TestPage.php @@ -5,26 +5,44 @@ use Brain\Monkey\Functions; use lloc\Msls\Admin\TranslationPicker\Page; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestPage extends MslsUnitTestCase { - public function test_page_slug_includes_post_type(): void { - $this->assertSame( 'msls-translation-picker-post', Page::page_slug( 'post' ) ); - $this->assertSame( 'msls-translation-picker-page', Page::page_slug( 'page' ) ); - $this->assertSame( 'msls-translation-picker-event', Page::page_slug( 'event' ) ); + /** + * @return array + */ + public static function page_slug_provider(): array { + return array( + 'post' => array( 'post', 'msls-translation-picker-post' ), + 'page' => array( 'page', 'msls-translation-picker-page' ), + 'event' => array( 'event', 'msls-translation-picker-event' ), + ); } - public function test_parent_slug_for_built_in_post(): void { - $this->assertSame( 'edit.php', Page::parent_slug( 'post' ) ); + #[DataProvider( 'page_slug_provider' )] + public function test_page_slug_includes_post_type( string $post_type, string $expected ): void { + $this->assertSame( $expected, Page::page_slug( $post_type ) ); } - public function test_parent_slug_for_other_post_types(): void { - $this->assertSame( 'edit.php?post_type=page', Page::parent_slug( 'page' ) ); - $this->assertSame( 'edit.php?post_type=event', Page::parent_slug( 'event' ) ); + /** + * Only the built-in post type lives on the bare edit.php, and an empty post type has + * no parent at all. + * + * @return array + */ + public static function parent_slug_provider(): array { + return array( + 'built-in post' => array( 'post', 'edit.php' ), + 'page' => array( 'page', 'edit.php?post_type=page' ), + 'custom type' => array( 'event', 'edit.php?post_type=event' ), + 'empty post type' => array( '', '' ), + ); } - public function test_parent_slug_for_empty_post_type(): void { - $this->assertSame( '', Page::parent_slug( '' ) ); + #[DataProvider( 'parent_slug_provider' )] + public function test_parent_slug( string $post_type, string $expected ): void { + $this->assertSame( $expected, Page::parent_slug( $post_type ) ); } public function test_url_uses_admin_url_and_query_arg(): void { @@ -71,23 +89,22 @@ function ( $args, $url ) { ); } - public function test_save_per_page_option_returns_int_for_picker_option(): void { - $this->assertSame( - 42, - Page::save_per_page_option( false, 'msls_tp_per_page', '42' ) - ); - } - - public function test_save_per_page_option_falls_back_for_non_positive(): void { - $this->assertSame( - Page::PER_PAGE_DEFAULT, - Page::save_per_page_option( false, 'msls_tp_per_page', '0' ) + /** + * @return array + */ + public static function save_per_page_option_provider(): array { + return array( + 'picker option' => array( 'msls_tp_per_page', '42', 42 ), + 'non-positive value' => array( 'msls_tp_per_page', '0', Page::PER_PAGE_DEFAULT ), + 'unrelated option ignored' => array( 'unrelated_option', '5', false ), ); } - public function test_save_per_page_option_passes_through_other_options(): void { - $this->assertFalse( - Page::save_per_page_option( false, 'unrelated_option', '5' ) - ); + /** + * @param int|false $expected + */ + #[DataProvider( 'save_per_page_option_provider' )] + public function test_save_per_page_option( string $option, string $value, $expected ): void { + $this->assertSame( $expected, Page::save_per_page_option( false, $option, $value ) ); } } diff --git a/tests/phpunit/Blog/TestBlog.php b/tests/phpunit/Blog/TestBlog.php index e28aab203..8ee9dee6d 100644 --- a/tests/phpunit/Blog/TestBlog.php +++ b/tests/phpunit/Blog/TestBlog.php @@ -7,6 +7,7 @@ use lloc\Msls\Blog\Collection; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestBlog extends MslsUnitTestCase { @@ -137,25 +138,22 @@ public function test_get_title(): void { } /** - * Dataprovider - * - * @return array[] + * @return array */ - public static function compareProvider(): array { + public static function compare_provider(): array { return array( - array( 0, 0, 0 ), - array( 0, 1, - 1 ), - array( 1, 0, 1 ), - array( - 1, - 2, 1 ), - array( - 2, - 1, - 1 ), + 'equal values' => array( 0, 0, 0 ), + 'a below b' => array( 0, 1, - 1 ), + 'a above b' => array( 1, 0, 1 ), + 'negative a above negative b' => array( - 1, - 2, 1 ), + 'negative a below negative b' => array( - 2, - 1, - 1 ), ); } /** * Verify the _cmp-method - * - * @dataProvider compareProvider */ + #[DataProvider( 'compare_provider' )] public function test__cmp_method( int $a, int $b, int $expected ): void { $this->assertEquals( $expected, Blog::internal_cmp( $a, $b ) ); diff --git a/tests/phpunit/Blog/TestCollection.php b/tests/phpunit/Blog/TestCollection.php index 91947b387..017667def 100644 --- a/tests/phpunit/Blog/TestCollection.php +++ b/tests/phpunit/Blog/TestCollection.php @@ -7,6 +7,7 @@ use lloc\Msls\Blog\Collection; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestCollection extends MslsUnitTestCase { @@ -84,18 +85,32 @@ function ( $blog_id, $option ) { ); } - public function test_get_configured_blog_description_empty(): void { + /** + * The second column is the fallback description, false where the call passes none. + * + * @return array + */ + public static function configured_blog_description_provider(): array { + return array( + 'unknown blog falls back to the given description' => array( 0, 'Test', 'Test' ), + 'german blog' => array( 1, false, 'Deutsch' ), + 'italian blog' => array( 2, false, 'Italiano' ), + 'french blog' => array( 3, false, 'Français' ), + 'unknown blog without a fallback' => array( 4, false, false ), + ); + } + + /** + * @param string|false $description + * @param string|false $expected + */ + #[DataProvider( 'configured_blog_description_provider' )] + public function test_get_configured_blog_description( int $blog_id, $description, $expected ): void { Functions\expect( 'get_site_option' )->once()->andReturn( array() ); $obj = new Collection(); - $this->assertEquals( 'Test', $obj->get_configured_blog_description( 0, 'Test' ) ); - - $this->assertEquals( 'Deutsch', $obj->get_configured_blog_description( 1 ) ); - $this->assertEquals( 'Italiano', $obj->get_configured_blog_description( 2 ) ); - $this->assertEquals( 'Français', $obj->get_configured_blog_description( 3 ) ); - - $this->assertFalse( $obj->get_configured_blog_description( 4 ) ); + $this->assertSame( $expected, $obj->get_configured_blog_description( $blog_id, $description ) ); } public function test_get_blogs_of_reference_user(): void { @@ -199,15 +214,24 @@ public function test_is_plugin_active_networkwide(): void { $this->assertTrue( $obj->is_plugin_active( 4 ) ); } - public function test_is_plugin_active(): void { + /** + * @return array + */ + public static function is_plugin_active_provider(): array { + return array( + 'configured blog' => array( 1, true ), + 'second blog' => array( 2, true ), + 'unconfigured blog' => array( 3, false ), + ); + } + + #[DataProvider( 'is_plugin_active_provider' )] + public function test_is_plugin_active( int $blog_id, bool $expected ): void { Functions\expect( 'get_site_option' )->once()->andReturn( array() ); $obj = new Collection(); - $this->assertTrue( $obj->is_plugin_active( 1 ) ); - $this->assertTrue( $obj->is_plugin_active( 2 ) ); - - $this->assertFalse( $obj->is_plugin_active( 3 ) ); + $this->assertSame( $expected, $obj->is_plugin_active( $blog_id ) ); } public function test_get_plugin_active_blogs(): void { @@ -260,26 +284,46 @@ public function test_get_current_blog(): void { $this->assertInstanceOf( Blog::class, $obj->get_current_blog() ); } - public function test_get_blog_language(): void { + /** + * A null blog id means the call passes no argument at all. + * + * @return array + */ + public static function blog_language_provider(): array { + return array( + 'german blog' => array( 1, 'de_DE' ), + 'italian blog' => array( 2, 'it_IT' ), + 'french blog' => array( 3, 'fr_FR' ), + 'current blog by default' => array( null, 'de_DE' ), + ); + } + + #[DataProvider( 'blog_language_provider' )] + public function test_get_blog_language( ?int $blog_id, string $expected ): void { Functions\expect( 'get_site_option' )->once()->andReturn( array() ); $obj = new Collection(); - $this->assertEquals( 'de_DE', $obj->get_blog_language( 1 ) ); - $this->assertEquals( 'it_IT', $obj->get_blog_language( 2 ) ); - $this->assertEquals( 'fr_FR', $obj->get_blog_language( 3 ) ); + $this->assertSame( $expected, $obj->get_blog_language( $blog_id ) ); + } - $this->assertEquals( 'de_DE', $obj->get_blog_language() ); + /** + * @return array + */ + public static function blog_id_provider(): array { + return array( + 'german' => array( 'de_DE', 1 ), + 'italian' => array( 'it_IT', 2 ), + 'not a msls blog' => array( 'fr_FR', null ), + ); } - public function test_get_blog_id(): void { + #[DataProvider( 'blog_id_provider' )] + public function test_get_blog_id( string $language, ?int $expected ): void { Functions\expect( 'get_site_option' )->once()->andReturn( array() ); $obj = new Collection(); - $this->assertEquals( 1, $obj->get_blog_id( 'de_DE' ) ); - $this->assertEquals( 2, $obj->get_blog_id( 'it_IT' ) ); - - $this->assertNull( $obj->get_blog_id( 'fr_FR' ) ); + $this->assertSame( $expected, $obj->get_blog_id( $language ) ); } } diff --git a/tests/phpunit/Compat/TestAliases.php b/tests/phpunit/Compat/TestAliases.php index 753b4e72b..f8fcd1d4d 100644 --- a/tests/phpunit/Compat/TestAliases.php +++ b/tests/phpunit/Compat/TestAliases.php @@ -7,17 +7,39 @@ use lloc\MslsTests\MslsUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\PreserveGlobalState; -use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; /** * Guards the add-on contract: every pre-3.0 name has to resolve through the autoloader * alone, at any time and in any plugin load order. MslsMenu and friends decide whether to * boot with a bare class_exists( lloc\Msls\MslsOptions::class ). + * + * Only the two tests that assert the *absence* of an alias need a pristine process: once + * any test has autoloaded a legacy name, the alias exists for the rest of the process. + * Everything else shares one process, which is why register_once() exists. */ -#[RunTestsInSeparateProcesses] #[PreserveGlobalState( false )] final class TestAliases extends MslsUnitTestCase { + /** + * @var bool Whether Aliases::register() already ran in this process. + */ + private static bool $registered = false; + + /** + * A second Aliases::register() would warn on every class_alias() and register the + * autoloader twice, so the shared process registers once. + */ + private static function register_once(): void { + if ( self::$registered ) { + return; + } + + self::$registered = true; + + Aliases::register(); + } + /** * @return array */ @@ -36,7 +58,7 @@ public static function alias_provider(): array { */ #[DataProvider( 'alias_provider' )] public function test_legacy_name_resolves( string $legacy, string $current ): void { - Aliases::register(); + self::register_once(); $this->assertTrue( class_exists( $legacy ) || interface_exists( $legacy ), @@ -53,20 +75,24 @@ class_exists( $legacy ) || interface_exists( $legacy ), * PHP resolves the class named in a type declaration without autoloading, so every * name an add-on may have put in one has to exist the moment register() returns. * - * @param class-string $current + * Runs isolated and checks the whole map at once: in a shared process the names + * autoloaded by test_legacy_name_resolves() would satisfy the assertion even if + * register() stopped creating them upfront. */ - #[DataProvider( 'alias_provider' )] - public function test_legacy_name_is_created_eagerly( string $legacy, string $current ): void { - if ( in_array( $legacy, Aliases::LAZY_ONLY, true ) ) { - $this->markTestSkipped( sprintf( '%s never shipped before 3.0 and stays lazy.', $legacy ) ); - } + #[RunInSeparateProcess] + public function test_shipped_names_are_created_eagerly(): void { + self::register_once(); - Aliases::register(); + foreach ( Aliases::MAP as $legacy => $current ) { + if ( in_array( $legacy, Aliases::LAZY_ONLY, true ) ) { + continue; + } - $this->assertTrue( - class_exists( $legacy, false ) || interface_exists( $legacy, false ), - sprintf( '%s has to be aliased without autoloading, not on demand.', $legacy ) - ); + $this->assertTrue( + class_exists( $legacy, false ) || interface_exists( $legacy, false ), + sprintf( '%s has to be aliased without autoloading, not on demand.', $legacy ) + ); + } } /** @@ -75,7 +101,7 @@ class_exists( $legacy, false ) || interface_exists( $legacy, false ), * returns, an instance of lloc\Msls\Frontend\Output. */ public function test_legacy_name_satisfies_a_return_type(): void { - Aliases::register(); + self::register_once(); $output = \Mockery::mock( Output::class ); @@ -83,13 +109,14 @@ public function test_legacy_name_satisfies_a_return_type(): void { } public function test_unknown_name_is_left_alone(): void { - Aliases::register(); + self::register_once(); $this->assertFalse( class_exists( 'lloc\Msls\MslsThisNeverExisted' ) ); } + #[RunInSeparateProcess] public function test_lazy_only_names_are_not_loaded_upfront(): void { - Aliases::register(); + self::register_once(); foreach ( Aliases::LAZY_ONLY as $legacy ) { $this->assertFalse( diff --git a/tests/phpunit/Component/Icon/TestIconPng.php b/tests/phpunit/Component/Icon/TestIconPng.php index 19a0e32bf..5287acae7 100644 --- a/tests/phpunit/Component/Icon/TestIconPng.php +++ b/tests/phpunit/Component/Icon/TestIconPng.php @@ -5,17 +5,28 @@ use Brain\Monkey\Functions; use lloc\Msls\Component\Icon\IconPng; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestIconPng extends MslsUnitTestCase { - public function test_get(): void { + /** + * @return array + */ + public static function locale_provider(): array { + return array( + 'cs_CZ' => array( 'cs_CZ', 'cz.png' ), + 'eo' => array( 'eo', 'europeanunion.png' ), + 'ca' => array( 'ca', 'catalonia.png' ), + 'pinko' => array( 'pinko', 'ko.png' ), + ); + } + + #[DataProvider( 'locale_provider' )] + public function test_get( string $locale, string $expected ): void { Functions\when( 'plugin_dir_path' )->justReturn( dirname( __DIR__, 4 ) . '/' ); $obj = new IconPng(); - $this->assertEquals( 'cz.png', $obj->get( 'cs_CZ' ) ); - $this->assertEquals( 'europeanunion.png', $obj->get( 'eo' ) ); - $this->assertEquals( 'catalonia.png', $obj->get( 'ca' ) ); - $this->assertEquals( 'ko.png', $obj->get( 'pinko' ) ); + $this->assertEquals( $expected, $obj->get( $locale ) ); } } diff --git a/tests/phpunit/Component/Icon/TestIconSvg.php b/tests/phpunit/Component/Icon/TestIconSvg.php index 03c71dd89..75196033c 100644 --- a/tests/phpunit/Component/Icon/TestIconSvg.php +++ b/tests/phpunit/Component/Icon/TestIconSvg.php @@ -5,17 +5,28 @@ use Brain\Monkey\Functions; use lloc\Msls\Component\Icon\IconSvg; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestIconSvg extends MslsUnitTestCase { - public function test_get(): void { + /** + * @return array + */ + public static function locale_provider(): array { + return array( + 'cs_CZ' => array( 'cs_CZ', 'flag-icon-cz' ), + 'eo' => array( 'eo', 'flag-icon-eu' ), + 'ca' => array( 'ca', 'flag-icon-es-ca' ), + 'pinko' => array( 'pinko', 'flag-icon-ko' ), + ); + } + + #[DataProvider( 'locale_provider' )] + public function test_get( string $locale, string $expected ): void { Functions\when( 'plugin_dir_path' )->justReturn( dirname( __DIR__, 4 ) . '/' ); $obj = new IconSvg(); - $this->assertEquals( 'flag-icon-cz', $obj->get( 'cs_CZ' ) ); - $this->assertEquals( 'flag-icon-eu', $obj->get( 'eo' ) ); - $this->assertEquals( 'flag-icon-es-ca', $obj->get( 'ca' ) ); - $this->assertEquals( 'flag-icon-ko', $obj->get( 'pinko' ) ); + $this->assertEquals( $expected, $obj->get( $locale ) ); } } diff --git a/tests/phpunit/ContentImport/TestAttachmentPathFinder.php b/tests/phpunit/ContentImport/TestAttachmentPathFinder.php index cb19084a9..9282c2da8 100644 --- a/tests/phpunit/ContentImport/TestAttachmentPathFinder.php +++ b/tests/phpunit/ContentImport/TestAttachmentPathFinder.php @@ -5,11 +5,19 @@ use lloc\Msls\ContentImport\AttachmentPathFinder; use lloc\MslsTests\MslsUnitTestCase; use Brain\Monkey\Functions; +use PHPUnit\Framework\Attributes\DataProvider; final class TestAttachmentPathFinder extends MslsUnitTestCase { - public static function dataprovider_filter_srcset(): array { + /** + * Every row supplies the full column set: the trailing numbers are Mockery invocation + * counts for get_post_meta(), delete_post_meta() and get_blog_post(). + * + * @return array>, string, mixed, array>, array|null, int, int, int, object|null}> + */ + public static function filter_srcset_provider(): array { $image_src = 'http://example.com/image.jpg'; + $sized_src = 'http://example.com/image-300x300.jpg'; $msls_imported = array( 'blog' => 1, 'post' => 1, @@ -17,20 +25,18 @@ public static function dataprovider_filter_srcset(): array { $source_post = (object) array( 'guid' => 'http://example.com/image.jpg' ); return array( - array( array(), $image_src, 0, array() ), - array( array(), $image_src, '', array() ), - array( array(), $image_src, null, array() ), - array( array(), $image_src, 1, array(), null, 1, 1 ), - array( array(), $image_src, 1, array(), array( 'random' => 'item' ), 1, 1 ), - array( array( array( 'url' => $image_src ) ), $image_src, 1, array( array( 'url' => $image_src ) ), $msls_imported, 1, 0, 1 ), - array( array( array( 'url' => $image_src ) ), $image_src, 1, array( array( 'url' => $image_src ) ), $msls_imported, 1, 0, 1, $source_post ), - array( array( array( 'url' => 'http://example.com/image-300x300.jpg' ) ), $image_src, 1, array( array( 'url' => 'http://example.com/image-300x300.jpg' ) ), $msls_imported, 1, 0, 1, $source_post ), + 'attachment id zero' => array( array(), $image_src, 0, array(), null, 0, 0, 0, null ), + 'attachment id empty string' => array( array(), $image_src, '', array(), null, 0, 0, 0, null ), + 'attachment id null' => array( array(), $image_src, null, array(), null, 0, 0, 0, null ), + 'no import meta' => array( array(), $image_src, 1, array(), null, 1, 1, 0, null ), + 'import meta without blog and post' => array( array(), $image_src, 1, array(), array( 'random' => 'item' ), 1, 1, 0, null ), + 'source post gone' => array( array( array( 'url' => $image_src ) ), $image_src, 1, array( array( 'url' => $image_src ) ), $msls_imported, 1, 0, 1, null ), + 'source url rewritten to itself' => array( array( array( 'url' => $image_src ) ), $image_src, 1, array( array( 'url' => $image_src ) ), $msls_imported, 1, 0, 1, $source_post ), + 'sized source url keeps its size' => array( array( array( 'url' => $sized_src ) ), $image_src, 1, array( array( 'url' => $sized_src ) ), $msls_imported, 1, 0, 1, $source_post ), ); } - /** - * @dataProvider dataprovider_filter_srcset - */ + #[DataProvider( 'filter_srcset_provider' )] public function test_filter_srcset( $source, $imageSrc, $attachmentId, $expected, $msls_imported = null, $times_gpm = 0, $time_dpm = 0, $times_gbp = 0, $blog_post = null ) { Functions\expect( 'get_post_meta' )->times( $times_gpm )->andReturn( $msls_imported ); Functions\expect( 'delete_post_meta' )->times( $time_dpm ); @@ -41,22 +47,23 @@ public function test_filter_srcset( $source, $imageSrc, $attachmentId, $expected $this->assertEquals( $expected, $test->filter_srcset( $source, null, $imageSrc, null, $attachmentId ) ); } - public static function dataprovider_filter_attachement_url(): array { + /** + * @return array + */ + public static function filter_attachment_url_provider(): array { $generic_obj = (object) array( 'guid' => 'http://example.com/image.jpg' ); $post_mock = \Mockery::mock( '\WP_Post' ); $post_mock->guid = 'http://example.com/image.jpg'; return array( - array( 'http://example.com/image.jpg', $generic_obj, 42 ), - array( 'http://example.com/image.jpg', $generic_obj, 0 ), - array( 'http://example.com/image.jpg', $post_mock, 42 ), + 'source post is not a WP_Post' => array( 'http://example.com/image.jpg', $generic_obj, 42 ), + 'no import data for the id' => array( 'http://example.com/image.jpg', $generic_obj, 0 ), + 'source post is a WP_Post' => array( 'http://example.com/image.jpg', $post_mock, 42 ), ); } - /** - * @dataProvider dataprovider_filter_attachement_url - */ + #[DataProvider( 'filter_attachment_url_provider' )] public function test_filter_attachment_url( string $image_src, $source_post, int $attachment_id ): void { $msls_imported = array( 'blog' => 1, diff --git a/tests/phpunit/ContentImport/TestImportCoordinates.php b/tests/phpunit/ContentImport/TestImportCoordinates.php index 6e540d89a..696aae94e 100644 --- a/tests/phpunit/ContentImport/TestImportCoordinates.php +++ b/tests/phpunit/ContentImport/TestImportCoordinates.php @@ -5,6 +5,7 @@ use lloc\Msls\ContentImport\ImportCoordinates; use lloc\MslsTests\MslsUnitTestCase; use Brain\Monkey\Functions; +use PHPUnit\Framework\Attributes\DataProvider; final class TestImportCoordinates extends MslsUnitTestCase { @@ -22,22 +23,23 @@ public function ImportCoordinatesFactory(): ImportCoordinates { return $test; } - public static function providerValidate(): array { + /** + * @return array + */ + public static function validate_provider(): array { $post = \Mockery::mock( \WP_Post::class ); return array( - array( null, null, null, null, null, false ), - array( $post, null, null, null, null, false ), - array( $post, $post, null, null, null, false ), - array( $post, $post, $post, null, null, false ), - array( $post, $post, $post, 'de_DE', null, false ), - array( $post, $post, $post, 'de_DE', 'it_IT', true ), + 'source post not found' => array( null, null, null, null, null, false ), + 'destination post not found' => array( $post, null, null, null, null, false ), + 'source post is not a WP_Post' => array( $post, $post, null, null, null, false ), + 'source language mismatch' => array( $post, $post, $post, null, null, false ), + 'destination language mismatch' => array( $post, $post, $post, 'de_DE', null, false ), + 'all coordinates valid' => array( $post, $post, $post, 'de_DE', 'it_IT', true ), ); } - /** - * @dataProvider providerValidate - */ + #[DataProvider( 'validate_provider' )] public function testValidate( $post_a, $post_b, $source_post, $lang_a, $lang_b, $expected ): void { Functions\expect( 'get_blog_post' )->andReturn( $post_a, $post_b ); Functions\expect( 'get_blog_option' )->andReturn( $lang_a, $lang_b ); diff --git a/tests/phpunit/ContentImport/TestImportLogger.php b/tests/phpunit/ContentImport/TestImportLogger.php index 0707fa7b1..ed0152f46 100644 --- a/tests/phpunit/ContentImport/TestImportLogger.php +++ b/tests/phpunit/ContentImport/TestImportLogger.php @@ -5,20 +5,22 @@ use lloc\Msls\ContentImport\ImportCoordinates; use lloc\Msls\ContentImport\ImportLogger; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestImportLogger extends MslsUnitTestCase { - public static function provider_get_data(): array { + /** + * @return array + */ + public static function get_data_provider(): array { return array( - array( 'info' ), - array( 'error' ), - array( 'success' ), + 'info' => array( 'info' ), + 'error' => array( 'error' ), + 'success' => array( 'success' ), ); } - /** - * @dataProvider provider_get_data - */ + #[DataProvider( 'get_data_provider' )] public function test_get_data( $key ): void { $coordinates = \Mockery::mock( ImportCoordinates::class ); $test = new ImportLogger( $coordinates ); diff --git a/tests/phpunit/Frontend/Map/TestHrefLang.php b/tests/phpunit/Frontend/Map/TestHrefLang.php index acfdae1d1..4acdc4746 100644 --- a/tests/phpunit/Frontend/Map/TestHrefLang.php +++ b/tests/phpunit/Frontend/Map/TestHrefLang.php @@ -8,6 +8,7 @@ use lloc\Msls\Blog\Blog; use lloc\Msls\Blog\Collection; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestHrefLang extends MslsUnitTestCase { @@ -36,16 +37,26 @@ private function HrefLangFactory(): HrefLang { return new HrefLang( $collection ); } - public function test_get(): void { + /** + * @return array + */ + public static function hreflang_provider(): array { + return array( + 'de_DE' => array( 'de_DE', 'de-DE' ), + 'de_DE_formal' => array( 'de_DE_formal', 'de-DE' ), + 'fr_FR' => array( 'fr_FR', 'fr' ), + 'es_ES' => array( 'es_ES', 'es' ), + 'cat' => array( 'cat', 'cat' ), + 'en_GB' => array( 'en_GB', 'en-GB' ), + 'en_US' => array( 'en_US', 'en-US' ), + ); + } + + #[DataProvider( 'hreflang_provider' )] + public function test_get( string $locale, string $expected ): void { $test = $this->HrefLangFactory(); - $this->assertEquals( 'de-DE', $test->get( 'de_DE' ) ); - $this->assertEquals( 'de-DE', $test->get( 'de_DE_formal' ) ); - $this->assertEquals( 'fr', $test->get( 'fr_FR' ) ); - $this->assertEquals( 'es', $test->get( 'es_ES' ) ); - $this->assertEquals( 'cat', $test->get( 'cat' ) ); - $this->assertEquals( 'en-GB', $test->get( 'en_GB' ) ); - $this->assertEquals( 'en-US', $test->get( 'en_US' ) ); + $this->assertEquals( $expected, $test->get( $locale ) ); } public function test_get_has_filter(): void { diff --git a/tests/phpunit/Frontend/TestContentFilter.php b/tests/phpunit/Frontend/TestContentFilter.php index 89805c7c7..455563bb4 100644 --- a/tests/phpunit/Frontend/TestContentFilter.php +++ b/tests/phpunit/Frontend/TestContentFilter.php @@ -9,6 +9,7 @@ use lloc\Msls\Blog\Collection; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestContentFilter extends MslsUnitTestCase { @@ -23,20 +24,23 @@ public function test_init(): void { ContentFilter::init(); } - public static function provide_content_filter_data(): array { + /** + * The truth table of the three conditions that keep content_filter() from adding a hint. + * + * @return array + */ + public static function content_filter_provider(): array { return array( - array( 'Test', 'Test', true, false, false ), - array( 'Test', 'Test', false, false, false ), - array( 'Test', 'Test', false, true, false ), - array( 'Test', 'Test', false, false, true ), - array( 'Test', 'Test', true, true, true ), + 'front page, not singular, filter off' => array( true, false, false ), + 'nothing enabled' => array( false, false, false ), + 'singular, filter off' => array( false, true, false ), + 'filter on, not singular' => array( false, false, true ), + 'front page, singular, filter on' => array( true, true, true ), ); } - /** - * @dataProvider provide_content_filter_data - */ - public function test_content_filter_empty( string $content, string $expected, bool $is_front_page, bool $is_singular, bool $is_content_filter ) { + #[DataProvider( 'content_filter_provider' )] + public function test_content_filter_empty( bool $is_front_page, bool $is_singular, bool $is_content_filter ) { Functions\when( 'is_front_page' )->justReturn( $is_front_page ); Functions\when( 'is_singular' )->justReturn( $is_singular ); @@ -45,7 +49,7 @@ public function test_content_filter_empty( string $content, string $expected, bo $test = new ContentFilter( $options ); - $this->assertEquals( $expected, $test->content_filter( $content ) ); + $this->assertEquals( 'Test', $test->content_filter( 'Test' ) ); } public function test_content_filter_one_link(): void { diff --git a/tests/phpunit/Frontend/TestOutput.php b/tests/phpunit/Frontend/TestOutput.php index c671c69ad..5cacf7be5 100644 --- a/tests/phpunit/Frontend/TestOutput.php +++ b/tests/phpunit/Frontend/TestOutput.php @@ -10,6 +10,7 @@ use lloc\Msls\Options\Options; use lloc\Msls\Options\Post\Post; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestOutput extends MslsUnitTestCase { @@ -30,116 +31,119 @@ public function test_get_method(): void { $this->assertEquals( array(), $test->get( 0 ) ); } - public function test_get_alternate_links_two_url(): void { - $blogs = array(); - - $a = \Mockery::mock( Blog::class ); - $a->shouldReceive( 'get_alpha2' )->andReturn( 'de' ); - $a->shouldReceive( 'get_language' )->andReturn( 'de_DE' ); - $a->shouldReceive( 'get_url' )->andReturn( 'https://example.de/' ); - $a->shouldReceive( 'get_description' )->andReturn( 'Deutsch' ); - - $blogs[] = $a; - - $b = \Mockery::mock( Blog::class ); - $b->shouldReceive( 'get_alpha2' )->andReturn( 'it' ); - $b->shouldReceive( 'get_language' )->andReturn( 'it_IT' ); - $b->shouldReceive( 'get_url' )->andReturn( 'https://example.it/' ); - $b->shouldReceive( 'get_description' )->andReturn( 'Italiano' ); - - $blogs[] = $b; - - $collection = \Mockery::mock( Collection::class ); - $collection->shouldReceive( 'get_objects' )->andReturn( $blogs ); - + /** + * The conditional context Output::get_alternate_links() walks before it looks at a + * single blog. Identical for every case below, so the cases only carry the blogs. + */ + private function expect_query_context( Collection $collection ): void { Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_admin' )->once()->andReturn( false ); - Functions\expect( 'is_front_page' )->once()->andReturn( false ); - Functions\expect( 'is_search' )->once()->andReturn( false ); - Functions\expect( 'is_404' )->once()->andReturn( false ); - Functions\expect( 'is_category' )->once()->andReturn( false ); - Functions\expect( 'is_tag' )->once()->andReturn( false ); - Functions\expect( 'is_tax' )->once()->andReturn( false ); - Functions\expect( 'is_date' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( false ); - Functions\expect( 'get_queried_object_id' )->once()->andReturn( 42 ); - Functions\expect( 'get_option' )->once()->andReturn( array() ); - - Filters\expectApplied( 'msls_output_get_alternate_links_arr' )->once(); - - $expected = - '' . PHP_EOL . - ''; - - $test = $this->OutputFactory(); - $this->assertEquals( $expected, $test->get_alternate_links() ); - } - - public function test_get_alternate_links_null_url(): void { - $blogs = array(); - - $a = \Mockery::mock( Blog::class ); - $a->shouldReceive( 'get_alpha2' )->andReturn( 'de' ); - $a->shouldReceive( 'get_language' )->andReturn( 'de_DE' ); - $a->shouldReceive( 'get_url' )->andReturnNull(); - - $blogs[] = $a; - - $collection = \Mockery::mock( Collection::class ); - $collection->shouldReceive( 'get_objects' )->andReturn( $blogs ); + $conditionals = array( + 'is_admin', + 'is_front_page', + 'is_search', + 'is_404', + 'is_category', + 'is_tag', + 'is_tax', + 'is_date', + 'is_author', + 'is_post_type_archive', + ); + + foreach ( $conditionals as $conditional ) { + Functions\expect( $conditional )->once()->andReturn( false ); + } - Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_admin' )->once()->andReturn( false ); - Functions\expect( 'is_front_page' )->once()->andReturn( false ); - Functions\expect( 'is_search' )->once()->andReturn( false ); - Functions\expect( 'is_404' )->once()->andReturn( false ); - Functions\expect( 'is_category' )->once()->andReturn( false ); - Functions\expect( 'is_tag' )->once()->andReturn( false ); - Functions\expect( 'is_tax' )->once()->andReturn( false ); - Functions\expect( 'is_date' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( false ); Functions\expect( 'get_queried_object_id' )->once()->andReturn( 42 ); Functions\expect( 'get_option' )->once()->andReturn( array() ); + } - $test = $this->OutputFactory(); - - $this->assertEquals( '', $test->get_alternate_links() ); + /** + * The second column is the filter the case expects to be applied, null where none is. + * + * @return array>, ?string, string}> + */ + public static function alternate_links_provider(): array { + $de = array( + 'alpha2' => 'de', + 'language' => 'de_DE', + 'url' => 'https://example.de/', + 'description' => 'Deutsch', + ); + + $it = array( + 'alpha2' => 'it', + 'language' => 'it_IT', + 'url' => 'https://example.it/', + 'description' => 'Italiano', + ); + + return array( + 'two blogs with an url' => array( + array( $de, $it ), + 'msls_output_get_alternate_links_arr', + '' . PHP_EOL . + '', + ), + 'a single blog becomes x-default' => array( + array( $de ), + 'msls_output_get_alternate_links_default', + '', + ), + 'blog without an url' => array( + array( + array( + 'alpha2' => 'de', + 'language' => 'de_DE', + 'url' => null, + ), + ), + null, + '', + ), + 'blog with an empty url' => array( + array( + array( + 'alpha2' => 'de', + 'language' => 'de_DE', + 'url' => '', + ), + ), + null, + '', + ), + ); } - public function test_get_alternate_links_one_url(): void { - $blogs = array(); + /** + * @param array> $blogs + */ + #[DataProvider( 'alternate_links_provider' )] + public function test_get_alternate_links( array $blogs, ?string $filter, string $expected ): void { + $objects = array(); - $a = \Mockery::mock( Blog::class ); - $a->shouldReceive( 'get_alpha2' )->andReturn( 'de' ); - $a->shouldReceive( 'get_language' )->andReturn( 'de_DE' ); - $a->shouldReceive( 'get_url' )->andReturn( 'https://example.de/' ); - $a->shouldReceive( 'get_description' )->andReturn( 'Deutsch' ); + foreach ( $blogs as $blog ) { + $mock = \Mockery::mock( Blog::class ); + $mock->shouldReceive( 'get_alpha2' )->andReturn( $blog['alpha2'] ); + $mock->shouldReceive( 'get_language' )->andReturn( $blog['language'] ); + $mock->shouldReceive( 'get_url' )->andReturn( $blog['url'] ); - $blogs[] = $a; + if ( isset( $blog['description'] ) ) { + $mock->shouldReceive( 'get_description' )->andReturn( $blog['description'] ); + } - $collection = \Mockery::mock( Collection::class ); - $collection->shouldReceive( 'get_objects' )->andReturn( $blogs ); + $objects[] = $mock; + } - Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_admin' )->once()->andReturn( false ); - Functions\expect( 'is_front_page' )->once()->andReturn( false ); - Functions\expect( 'is_search' )->once()->andReturn( false ); - Functions\expect( 'is_404' )->once()->andReturn( false ); - Functions\expect( 'is_category' )->once()->andReturn( false ); - Functions\expect( 'is_tag' )->once()->andReturn( false ); - Functions\expect( 'is_tax' )->once()->andReturn( false ); - Functions\expect( 'is_date' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( false ); - Functions\expect( 'get_queried_object_id' )->once()->andReturn( 42 ); - Functions\expect( 'get_option' )->once()->andReturn( array() ); + $collection = \Mockery::mock( Collection::class ); + $collection->shouldReceive( 'get_objects' )->andReturn( $objects ); - Filters\expectApplied( 'msls_output_get_alternate_links_default' )->once(); + $this->expect_query_context( $collection ); - $expected = ''; + if ( null !== $filter ) { + Filters\expectApplied( $filter )->once(); + } $test = $this->OutputFactory(); @@ -298,65 +302,36 @@ public function test_set_tags(): void { $this->assertInstanceOf( Output::class, $test->set_tags() ); } - public function test_is_requirements_not_fulfilled_with_null(): void { - $test = $this->OutputFactory(); - - $this->assertFalse( $test->is_requirements_not_fulfilled( null, false, 'de_DE' ) ); - $this->assertTrue( $test->is_requirements_not_fulfilled( null, true, 'de_DE' ) ); + /** + * @return array + */ + public static function requirements_provider(): array { + return array( + 'no data, translations optional' => array( null, false, false ), + 'no data, translations required' => array( null, true, true ), + 'options, translations optional' => array( Options::class, false, false ), + 'options, translations required' => array( Options::class, true, false ), + 'post options, optional' => array( Post::class, false, false ), + 'post options without translations' => array( Post::class, true, true ), + ); } - public function test_is_requirements_not_fulfilled_with_mslsoptions(): void { - Functions\expect( 'get_option' )->once()->andReturn( array() ); - - $mydata = new Options(); - - $test = $this->OutputFactory(); - - $this->assertFalse( $test->is_requirements_not_fulfilled( $mydata, false, 'de_DE' ) ); - $this->assertFalse( $test->is_requirements_not_fulfilled( $mydata, true, 'de_DE' ) ); - } - - public function test_is_requirements_not_fulfilled_with_mslsoptionspost(): void { - Functions\expect( 'get_option' )->once()->andReturn( array() ); + /** + * @param ?class-string $data_class + */ + #[DataProvider( 'requirements_provider' )] + public function test_is_requirements_not_fulfilled( ?string $data_class, bool $only_with_translation, bool $expected ): void { + $mydata = null; - $mydata = new Post(); + if ( null !== $data_class ) { + Functions\expect( 'get_option' )->once()->andReturn( array() ); - $test = $this->OutputFactory(); - - $this->assertFalse( $test->is_requirements_not_fulfilled( $mydata, false, 'de_DE' ) ); - $this->assertTrue( $test->is_requirements_not_fulfilled( $mydata, true, 'de_DE' ) ); - } - - public function test_get_alternate_links_empty_url(): void { - $blogs = array(); - - $a = \Mockery::mock( Blog::class ); - $a->shouldReceive( 'get_alpha2' )->andReturn( 'de' ); - $a->shouldReceive( 'get_language' )->andReturn( 'de_DE' ); - $a->shouldReceive( 'get_url' )->andReturn( '' ); - - $blogs[] = $a; - - $collection = \Mockery::mock( Collection::class ); - $collection->shouldReceive( 'get_objects' )->andReturn( $blogs ); - - Functions\expect( 'msls_blog_collection' )->once()->andReturn( $collection ); - Functions\expect( 'is_admin' )->once()->andReturn( false ); - Functions\expect( 'is_front_page' )->once()->andReturn( false ); - Functions\expect( 'is_search' )->once()->andReturn( false ); - Functions\expect( 'is_404' )->once()->andReturn( false ); - Functions\expect( 'is_category' )->once()->andReturn( false ); - Functions\expect( 'is_tag' )->once()->andReturn( false ); - Functions\expect( 'is_tax' )->once()->andReturn( false ); - Functions\expect( 'is_date' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( false ); - Functions\expect( 'get_queried_object_id' )->once()->andReturn( 42 ); - Functions\expect( 'get_option' )->once()->andReturn( array() ); + $mydata = new $data_class(); + } $test = $this->OutputFactory(); - $this->assertEquals( '', $test->get_alternate_links() ); + $this->assertSame( $expected, $test->is_requirements_not_fulfilled( $mydata, $only_with_translation, 'de_DE' ) ); } public function test_get_skips_empty_url(): void { diff --git a/tests/phpunit/Frontend/TestWidget.php b/tests/phpunit/Frontend/TestWidget.php index 60499bb26..e6294d0a7 100644 --- a/tests/phpunit/Frontend/TestWidget.php +++ b/tests/phpunit/Frontend/TestWidget.php @@ -8,6 +8,7 @@ use lloc\Msls\Blog\Collection; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestWidget extends MslsUnitTestCase { @@ -46,17 +47,20 @@ public function test_widget(): void { ( new Widget() )->widget( array(), array( 'title' => 'Test' ) ); } + /** + * The last column is the Mockery invocation count for wp_strip_all_tags(). + * + * @return array, array, array, int}> + */ public static function update_provider(): array { return array( - array( array(), array(), array(), 0 ), - array( array( 'title' => 'abc' ), array(), array( 'title' => 'abc' ), 1 ), - array( array( 'title' => 'xyz' ), array( 'title' => 'abc' ), array( 'title' => 'xyz' ), 1 ), + 'no title in either instance' => array( array(), array(), array(), 0 ), + 'title added' => array( array( 'title' => 'abc' ), array(), array( 'title' => 'abc' ), 1 ), + 'title replaced' => array( array( 'title' => 'xyz' ), array( 'title' => 'abc' ), array( 'title' => 'xyz' ), 1 ), ); } - /** - * @dataProvider update_provider - */ + #[DataProvider( 'update_provider' )] public function test_update( array $new_instance, array $old_instance, array $expected, int $times ): void { Functions\expect( 'wp_strip_all_tags' )->times( $times )->andReturnFirstArg(); diff --git a/tests/phpunit/Options/Query/TestQuery.php b/tests/phpunit/Options/Query/TestQuery.php index 0d303504a..8f3924732 100644 --- a/tests/phpunit/Options/Query/TestQuery.php +++ b/tests/phpunit/Options/Query/TestQuery.php @@ -11,8 +11,7 @@ use lloc\Msls\Options\Query\Query; use lloc\Msls\Options\Query\Year; use lloc\MslsTests\MslsUnitTestCase; - -use function Brain\Monkey\Functions; +use PHPUnit\Framework\Attributes\DataProvider; final class TestQuery extends MslsUnitTestCase { @@ -28,63 +27,57 @@ public function test_get_params(): void { $this->assertEquals( array(), Query::get_params() ); } - public function test_create_is_day(): void { - Functions\expect( 'is_day' )->once()->andReturn( true ); - Functions\expect( 'get_query_var' )->times( 6 )->andReturnValues( array( 1969, 6, 26 ) ); - Functions\expect( 'get_option' )->once(); - - $this->assertInstanceOf( Day::class, Query::create() ); + /** + * The order Query::create() asks WordPress in. Everything before the conditional that + * matches has to return false, so a case only has to name where the chain hits. + * + * @var array + */ + private const CREATE_CHAIN = array( 'is_day', 'is_month', 'is_year', 'is_author', 'is_post_type_archive' ); + + /** + * The third and fourth column are the getter Query::create() reads the archive from + * and how often it is called. + * + * @return array, ?class-string}> + */ + public static function create_provider(): array { + return array( + 'day archive' => array( 'is_day', 'get_query_var', 6, array( 1969, 6, 26 ), Day::class ), + 'month archive' => array( 'is_month', 'get_query_var', 4, array( 1969, 6 ), Month::class ), + 'year archive' => array( 'is_year', 'get_query_var', 2, array( 1969 ), Year::class ), + 'author archive' => array( 'is_author', 'get_queried_object_id', 2, array( 42 ), Author::class ), + 'post type archive' => array( 'is_post_type_archive', 'get_query_var', 2, array( 'book' ), PostType::class ), + 'no archive whatever' => array( null, null, 0, array(), null ), + ); } - public function test_create_is_month(): void { - Functions\expect( 'is_day' )->once()->andReturn( false ); - Functions\expect( 'is_month' )->once()->andReturn( true ); - Functions\expect( 'get_query_var' )->times( 4 )->andReturnValues( array( 1969, 6 ) ); - Functions\expect( 'get_option' )->once(); - $this->assertInstanceOf( Month::class, Query::create() ); - } + /** + * @param array $values + * @param ?class-string $expected + */ + #[DataProvider( 'create_provider' )] + public function test_create( ?string $matching, ?string $getter, int $getter_times, array $values, ?string $expected ): void { + foreach ( self::CREATE_CHAIN as $conditional ) { + Functions\expect( $conditional )->once()->andReturn( $matching === $conditional ); - public function test_create_is_year(): void { - Functions\expect( 'is_day' )->once()->andReturn( false ); - Functions\expect( 'is_month' )->once()->andReturn( false ); - Functions\expect( 'is_year' )->once()->andReturn( true ); - Functions\expect( 'get_query_var' )->times( 2 )->andReturn( 1969 ); - Functions\expect( 'get_option' )->once(); - - $this->assertInstanceOf( Year::class, Query::create() ); - } + if ( $matching === $conditional ) { + break; + } + } - public function test_create_is_author(): void { - Functions\expect( 'is_day' )->once()->andReturn( false ); - Functions\expect( 'is_month' )->once()->andReturn( false ); - Functions\expect( 'is_year' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( true ); - Functions\expect( 'get_queried_object_id' )->times( 2 )->andReturn( 42 ); - Functions\expect( 'get_option' )->once(); + if ( null !== $getter ) { + Functions\expect( $getter )->times( $getter_times )->andReturnValues( $values ); + Functions\expect( 'get_option' )->once(); + } - $this->assertInstanceOf( Author::class, Query::create() ); - } - - public function test_create_is_post_type_archive(): void { - Functions\expect( 'is_day' )->once()->andReturn( false ); - Functions\expect( 'is_month' )->once()->andReturn( false ); - Functions\expect( 'is_year' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( true ); - Functions\expect( 'get_query_var' )->times( 2 )->andReturn( 'book' ); - Functions\expect( 'get_option' )->once(); - - $this->assertInstanceOf( PostType::class, Query::create() ); - } + if ( null === $expected ) { + $this->assertNull( Query::create() ); - public function test_create_is_null(): void { - Functions\expect( 'is_day' )->once()->andReturn( false ); - Functions\expect( 'is_month' )->once()->andReturn( false ); - Functions\expect( 'is_year' )->once()->andReturn( false ); - Functions\expect( 'is_author' )->once()->andReturn( false ); - Functions\expect( 'is_post_type_archive' )->once()->andReturn( false ); + return; + } - $this->assertNull( Query::create() ); + $this->assertInstanceOf( $expected, Query::create() ); } public function test_current_get_postlink(): void { diff --git a/tests/phpunit/Options/TestOptions.php b/tests/phpunit/Options/TestOptions.php index 1710dc42f..75ecfc125 100644 --- a/tests/phpunit/Options/TestOptions.php +++ b/tests/phpunit/Options/TestOptions.php @@ -7,6 +7,7 @@ use lloc\Msls\ContentTypes\PostType; use lloc\Msls\Options\Options; use lloc\MslsTests\MslsUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class TestOptions extends MslsUnitTestCase { @@ -72,10 +73,13 @@ public function test_save(): void { $obj->save( $arr ); } + /** + * @return array + */ public static function set_provider(): array { return array( - array( true, array() ), - array( + 'empty array' => array( true, array() ), + 'filled array' => array( true, array( 'temp' => 'abc', @@ -83,17 +87,15 @@ public static function set_provider(): array { 'us' => 2, ), ), - array( false, 'Test' ), - array( false, 1 ), - array( false, 1.1 ), - array( false, null ), - array( false, new \stdClass() ), + 'string' => array( false, 'Test' ), + 'integer' => array( false, 1 ), + 'float' => array( false, 1.1 ), + 'null' => array( false, null ), + 'object' => array( false, new \stdClass() ), ); } - /** - * @dataProvider set_provider - */ + #[DataProvider( 'set_provider' )] function test_set( $expected, $input ): void { $obj = $this->MslsOptionsFactory(); @@ -190,30 +192,38 @@ public function test_get_icon_type_admin_display(): void { $this->assertEquals( MslsAdminIcon::TYPE_LABEL, $obj->get_icon_type() ); } - public static function provide_data_for_slug_check(): array { + /** + * The columns after $expected are $with_front, $is_subdomain_install, $using_permalinks, + * $permalink_structure and $is_main_site. + * + * The two 'blogg' rows differ only in $is_main_site: the lookalike slug is never + * stripped, so that flag cannot change the result. They are kept as a pair on purpose, + * to pin down that the main site does not get the prefix re-added either. + * + * @return array + */ + public static function slug_check_provider(): array { return array( - array( '', '', false, false, false, '', false ), // first return - array( null, '', false, false, false, '', false ), // first return - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, false, '', false ), // second return - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, false, true, '', false ), // second return - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '', false ), - array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/%year%/%monthnum%/%postname%/', false ), - array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/2024/05/test', true, true, true, '/blog/%year%/%monthnum%/%postname%/', false ), - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', false ), - array( 'https://msls.co/blog/test', 'https://msls.co/test', true, true, true, '/blog/%postname%/', false ), - array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/%year%/%monthnum%/%postname%/', true ), - array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/blog/%year%/%monthnum%/%postname%/', true ), - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', true ), - array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/blog/%postname%/', true ), - array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', false ), - array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', true ), - array( 'https://msls.co/blog/', 'https://msls.co/', true, true, true, '/blog/%postname%/', false ), + 'empty url returns early' => array( '', '', false, false, false, '', false ), + 'null url returns early' => array( null, '', false, false, false, '', false ), + 'subdomain install without permalinks' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, false, '', false ), + 'permalinks without subdomain install' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, false, true, '', false ), + 'no permalink structure' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '', false ), + 'dated structure without front, sub site' => array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/%year%/%monthnum%/%postname%/', false ), + 'dated structure with front, sub site' => array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/2024/05/test', true, true, true, '/blog/%year%/%monthnum%/%postname%/', false ), + 'postname structure without front, sub site' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', false ), + 'postname structure with front, sub site' => array( 'https://msls.co/blog/test', 'https://msls.co/test', true, true, true, '/blog/%postname%/', false ), + 'dated structure without front, main site' => array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/%year%/%monthnum%/%postname%/', true ), + 'dated structure with front, main site' => array( 'https://msls.co/blog/2024/05/test', 'https://msls.co/blog/2024/05/test', true, true, true, '/blog/%year%/%monthnum%/%postname%/', true ), + 'postname structure without front, main site' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/%postname%/', true ), + 'postname structure with front, main site' => array( 'https://msls.co/blog/test', 'https://msls.co/blog/test', true, true, true, '/blog/%postname%/', true ), + 'lookalike slug is left alone, sub site' => array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', false ), + 'lookalike slug is left alone, main site' => array( 'https://msls.co/blogg/', 'https://msls.co/blogg/', true, true, true, '/blog/%postname%/', true ), + 'bare front is stripped down to the home url' => array( 'https://msls.co/blog/', 'https://msls.co/', true, true, true, '/blog/%postname%/', false ), ); } - /** - * @dataProvider provide_data_for_slug_check - */ + #[DataProvider( 'slug_check_provider' )] public function test_check_for_blog_slug( ?string $url, string $expected, bool $with_front, bool $is_subdomain_install, bool $using_permalinks, string $permalink_structure, bool $is_main_site ): void { global $wp_rewrite, $current_site; diff --git a/tests/phpunit/TestDeprecated.php b/tests/phpunit/TestDeprecated.php index 158638596..a9b1bc83e 100644 --- a/tests/phpunit/TestDeprecated.php +++ b/tests/phpunit/TestDeprecated.php @@ -3,50 +3,32 @@ namespace lloc\MslsTests; use Brain\Monkey\Functions; +use PHPUnit\Framework\Attributes\DataProvider; final class TestDeprecated extends MslsUnitTestCase { - public function test_get_the_msls(): void { - Functions\expect( '_deprecated_function' )->once(); - Functions\expect( 'msls_get_switcher' )->once()->andReturn( '' ); - - get_the_msls( null ); - - $this->expectNotToPerformAssertions(); - } - - public function test_the_msls(): void { - Functions\expect( '_deprecated_function' )->once(); - Functions\expect( 'msls_the_switcher' )->once()->andReturn( '' ); - - the_msls(); - - $this->expectNotToPerformAssertions(); - } - - public function test_get_msls_flag_url(): void { - Functions\expect( '_deprecated_function' )->once(); - Functions\expect( 'msls_get_flag_url' )->once()->andReturn( '' ); - - get_msls_flag_url( 'en' ); - - $this->expectNotToPerformAssertions(); - } - - public function test_get_msls_blog_description(): void { - Functions\expect( '_deprecated_function' )->once(); - Functions\expect( 'msls_get_blog_description' )->once()->andReturn( '' ); - - get_msls_blog_description( 'en' ); - - $this->expectNotToPerformAssertions(); + /** + * @return array, string}> + */ + public static function deprecated_provider(): array { + return array( + 'get_the_msls' => array( 'get_the_msls', array( null ), 'msls_get_switcher' ), + 'the_msls' => array( 'the_msls', array(), 'msls_the_switcher' ), + 'get_msls_flag_url' => array( 'get_msls_flag_url', array( 'en' ), 'msls_get_flag_url' ), + 'get_msls_blog_description' => array( 'get_msls_blog_description', array( 'en' ), 'msls_get_blog_description' ), + 'get_msls_permalink' => array( 'get_msls_permalink', array( 'en' ), 'msls_get_permalink' ), + ); } - public function test_get_msls_permalink(): void { + /** + * @param array $args + */ + #[DataProvider( 'deprecated_provider' )] + public function test_legacy_function_warns_and_forwards( string $legacy, array $args, string $replacement ): void { Functions\expect( '_deprecated_function' )->once(); - Functions\expect( 'msls_get_permalink' )->once()->andReturn( '' ); + Functions\expect( $replacement )->once()->andReturn( '' ); - get_msls_permalink( 'en' ); + $legacy( ...$args ); $this->expectNotToPerformAssertions(); } From 76ca308abafc456b63b72d72506ecf61729c4013 Mon Sep 17 00:00:00 2001 From: Dennis Ploetner Date: Fri, 28 Aug 2026 13:01:14 +0200 Subject: [PATCH 2/2] Dataproviders reviewed --- .github/workflows/test.yml | 4 +- tests/phpunit/Admin/TestAdmin.php | 68 ++++++------------- tests/phpunit/Admin/TestIcon.php | 8 +-- .../Admin/TranslationPicker/TestPage.php | 18 +++-- tests/phpunit/Compat/TestAliases.php | 7 +- tests/phpunit/Component/Icon/TestIconPng.php | 18 +++-- tests/phpunit/Component/Icon/TestIconSvg.php | 18 +++-- .../ContentImport/TestImportLogger.php | 12 ++-- tests/phpunit/Frontend/Map/TestHrefLang.php | 24 ++++--- tests/phpunit/TestDeprecated.php | 23 +++++-- 10 files changed, 112 insertions(+), 88 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b774e780..a16762d98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,5 +42,7 @@ jobs: tools: composer:v2 - name: Install production dependencies run: composer install --no-dev --prefer-dist --no-progress - - name: Lint the shipped PHP files + # Syntax only. php -l rejects PHP 8 grammar on 7.4, but not calls to PHP 8 + # library functions: includes/ relies on WordPress polyfilling str_starts_with(). + - name: Lint the shipped PHP files for PHP 7.4 syntax run: git ls-files '*.php' | grep -vE '^(tests|bin)/' | xargs -n1 -P4 php -l diff --git a/tests/phpunit/Admin/TestAdmin.php b/tests/phpunit/Admin/TestAdmin.php index 1e539b767..c5fe46933 100644 --- a/tests/phpunit/Admin/TestAdmin.php +++ b/tests/phpunit/Admin/TestAdmin.php @@ -194,57 +194,33 @@ function ( array $list ): array { } /** - * Every one of these renders a single settings field and writes nothing else. + * Every one of these renders a single settings field and writes nothing else, so a + * case is just the Admin method and the markup it has to produce. * * @return array */ public static function settings_field_provider(): array { - return array( - 'activate_autocomplete' => array( - 'activate_autocomplete', - ' ', - ), - 'sort_by_description' => array( - 'sort_by_description', - ' ', - ), - 'exclude_current_blog' => array( - 'exclude_current_blog', - ' ', - ), - 'only_with_translation' => array( - 'only_with_translation', - ' ', - ), - 'output_current_blog' => array( - 'output_current_blog', - ' ', - ), - 'description' => array( - 'description', - '', - ), - 'before_output' => array( - 'before_output', - '', - ), - 'after_output' => array( - 'after_output', - '', - ), - 'before_item' => array( - 'before_item', - '', - ), - 'after_item' => array( - 'after_item', - '', - ), - 'content_filter' => array( - 'content_filter', - ' ', - ), + $fields = array( + 'activate_autocomplete' => ' ', + 'sort_by_description' => ' ', + 'exclude_current_blog' => ' ', + 'only_with_translation' => ' ', + 'output_current_blog' => ' ', + 'description' => '', + 'before_output' => '', + 'after_output' => '', + 'before_item' => '', + 'after_item' => '', + 'content_filter' => ' ', ); + + $data = array(); + + foreach ( $fields as $method => $expected ) { + $data[ $method ] = array( $method, $expected ); + } + + return $data; } #[DataProvider( 'settings_field_provider' )] diff --git a/tests/phpunit/Admin/TestIcon.php b/tests/phpunit/Admin/TestIcon.php index ad454a142..e9232895c 100644 --- a/tests/phpunit/Admin/TestIcon.php +++ b/tests/phpunit/Admin/TestIcon.php @@ -182,18 +182,18 @@ public function test_set_icon_type(): void { /** * A null language means none was set, which is what the empty expectation is about. * - * @return array + * @return array */ public static function icon_type_provider(): array { return array( - 'flag' => array( 'flag', 'de_DE', 'de_DE' ), - 'label' => array( 'label', 'it_IT', 'itIT' ), + 'flag with a language' => array( 'flag', 'de_DE', 'de_DE' ), + 'label with a language' => array( 'label', 'it_IT', 'itIT' ), 'flag without a language' => array( 'flag', null, '' ), ); } #[DataProvider( 'icon_type_provider' )] - public function test_get_icon( ?string $icon_type, ?string $language, string $expected ): void { + public function test_get_icon( string $icon_type, ?string $language, string $expected ): void { Functions\expect( 'plugin_dir_path' )->andReturn( dirname( __DIR__, 3 ) . '/' ); $obj = new Icon( 'post' ); diff --git a/tests/phpunit/Admin/TranslationPicker/TestPage.php b/tests/phpunit/Admin/TranslationPicker/TestPage.php index e84e22208..af85aaa01 100644 --- a/tests/phpunit/Admin/TranslationPicker/TestPage.php +++ b/tests/phpunit/Admin/TranslationPicker/TestPage.php @@ -13,11 +13,19 @@ final class TestPage extends MslsUnitTestCase { * @return array */ public static function page_slug_provider(): array { - return array( - 'post' => array( 'post', 'msls-translation-picker-post' ), - 'page' => array( 'page', 'msls-translation-picker-page' ), - 'event' => array( 'event', 'msls-translation-picker-event' ), + $slugs = array( + 'post' => 'msls-translation-picker-post', + 'page' => 'msls-translation-picker-page', + 'event' => 'msls-translation-picker-event', ); + + $data = array(); + + foreach ( $slugs as $post_type => $expected ) { + $data[ $post_type ] = array( $post_type, $expected ); + } + + return $data; } #[DataProvider( 'page_slug_provider' )] @@ -34,7 +42,7 @@ public function test_page_slug_includes_post_type( string $post_type, string $ex public static function parent_slug_provider(): array { return array( 'built-in post' => array( 'post', 'edit.php' ), - 'page' => array( 'page', 'edit.php?post_type=page' ), + 'built-in page' => array( 'page', 'edit.php?post_type=page' ), 'custom type' => array( 'event', 'edit.php?post_type=event' ), 'empty post type' => array( '', '' ), ); diff --git a/tests/phpunit/Compat/TestAliases.php b/tests/phpunit/Compat/TestAliases.php index f8fcd1d4d..6c0be0ee3 100644 --- a/tests/phpunit/Compat/TestAliases.php +++ b/tests/phpunit/Compat/TestAliases.php @@ -14,9 +14,10 @@ * alone, at any time and in any plugin load order. MslsMenu and friends decide whether to * boot with a bare class_exists( lloc\Msls\MslsOptions::class ). * - * Only the two tests that assert the *absence* of an alias need a pristine process: once - * any test has autoloaded a legacy name, the alias exists for the rest of the process. - * Everything else shares one process, which is why register_once() exists. + * The two tests whose verdict depends on nothing having touched the aliases yet run in a + * pristine process: once any test has autoloaded a legacy name, that alias exists for the + * rest of the process, and both of them would pass spuriously. Everything else shares one + * process, which is why register_once() exists. */ #[PreserveGlobalState( false )] final class TestAliases extends MslsUnitTestCase { diff --git a/tests/phpunit/Component/Icon/TestIconPng.php b/tests/phpunit/Component/Icon/TestIconPng.php index 5287acae7..8b9137642 100644 --- a/tests/phpunit/Component/Icon/TestIconPng.php +++ b/tests/phpunit/Component/Icon/TestIconPng.php @@ -13,12 +13,20 @@ final class TestIconPng extends MslsUnitTestCase { * @return array */ public static function locale_provider(): array { - return array( - 'cs_CZ' => array( 'cs_CZ', 'cz.png' ), - 'eo' => array( 'eo', 'europeanunion.png' ), - 'ca' => array( 'ca', 'catalonia.png' ), - 'pinko' => array( 'pinko', 'ko.png' ), + $locales = array( + 'cs_CZ' => 'cz.png', + 'eo' => 'europeanunion.png', + 'ca' => 'catalonia.png', + 'pinko' => 'ko.png', ); + + $data = array(); + + foreach ( $locales as $locale => $expected ) { + $data[ $locale ] = array( $locale, $expected ); + } + + return $data; } #[DataProvider( 'locale_provider' )] diff --git a/tests/phpunit/Component/Icon/TestIconSvg.php b/tests/phpunit/Component/Icon/TestIconSvg.php index 75196033c..77daf7d6d 100644 --- a/tests/phpunit/Component/Icon/TestIconSvg.php +++ b/tests/phpunit/Component/Icon/TestIconSvg.php @@ -13,12 +13,20 @@ final class TestIconSvg extends MslsUnitTestCase { * @return array */ public static function locale_provider(): array { - return array( - 'cs_CZ' => array( 'cs_CZ', 'flag-icon-cz' ), - 'eo' => array( 'eo', 'flag-icon-eu' ), - 'ca' => array( 'ca', 'flag-icon-es-ca' ), - 'pinko' => array( 'pinko', 'flag-icon-ko' ), + $locales = array( + 'cs_CZ' => 'flag-icon-cz', + 'eo' => 'flag-icon-eu', + 'ca' => 'flag-icon-es-ca', + 'pinko' => 'flag-icon-ko', ); + + $data = array(); + + foreach ( $locales as $locale => $expected ) { + $data[ $locale ] = array( $locale, $expected ); + } + + return $data; } #[DataProvider( 'locale_provider' )] diff --git a/tests/phpunit/ContentImport/TestImportLogger.php b/tests/phpunit/ContentImport/TestImportLogger.php index ed0152f46..6e54d89da 100644 --- a/tests/phpunit/ContentImport/TestImportLogger.php +++ b/tests/phpunit/ContentImport/TestImportLogger.php @@ -13,11 +13,13 @@ final class TestImportLogger extends MslsUnitTestCase { * @return array */ public static function get_data_provider(): array { - return array( - 'info' => array( 'info' ), - 'error' => array( 'error' ), - 'success' => array( 'success' ), - ); + $data = array(); + + foreach ( array( 'info', 'error', 'success' ) as $type ) { + $data[ $type ] = array( $type ); + } + + return $data; } #[DataProvider( 'get_data_provider' )] diff --git a/tests/phpunit/Frontend/Map/TestHrefLang.php b/tests/phpunit/Frontend/Map/TestHrefLang.php index 4acdc4746..540e080c0 100644 --- a/tests/phpunit/Frontend/Map/TestHrefLang.php +++ b/tests/phpunit/Frontend/Map/TestHrefLang.php @@ -41,15 +41,23 @@ private function HrefLangFactory(): HrefLang { * @return array */ public static function hreflang_provider(): array { - return array( - 'de_DE' => array( 'de_DE', 'de-DE' ), - 'de_DE_formal' => array( 'de_DE_formal', 'de-DE' ), - 'fr_FR' => array( 'fr_FR', 'fr' ), - 'es_ES' => array( 'es_ES', 'es' ), - 'cat' => array( 'cat', 'cat' ), - 'en_GB' => array( 'en_GB', 'en-GB' ), - 'en_US' => array( 'en_US', 'en-US' ), + $locales = array( + 'de_DE' => 'de-DE', + 'de_DE_formal' => 'de-DE', + 'fr_FR' => 'fr', + 'es_ES' => 'es', + 'cat' => 'cat', + 'en_GB' => 'en-GB', + 'en_US' => 'en-US', ); + + $data = array(); + + foreach ( $locales as $locale => $expected ) { + $data[ $locale ] = array( $locale, $expected ); + } + + return $data; } #[DataProvider( 'hreflang_provider' )] diff --git a/tests/phpunit/TestDeprecated.php b/tests/phpunit/TestDeprecated.php index a9b1bc83e..0a0f6dddb 100644 --- a/tests/phpunit/TestDeprecated.php +++ b/tests/phpunit/TestDeprecated.php @@ -8,16 +8,27 @@ final class TestDeprecated extends MslsUnitTestCase { /** + * Maps every deprecated global to the arguments it is called with and the function it + * has to forward to. + * * @return array, string}> */ public static function deprecated_provider(): array { - return array( - 'get_the_msls' => array( 'get_the_msls', array( null ), 'msls_get_switcher' ), - 'the_msls' => array( 'the_msls', array(), 'msls_the_switcher' ), - 'get_msls_flag_url' => array( 'get_msls_flag_url', array( 'en' ), 'msls_get_flag_url' ), - 'get_msls_blog_description' => array( 'get_msls_blog_description', array( 'en' ), 'msls_get_blog_description' ), - 'get_msls_permalink' => array( 'get_msls_permalink', array( 'en' ), 'msls_get_permalink' ), + $legacy_functions = array( + 'get_the_msls' => array( array( null ), 'msls_get_switcher' ), + 'the_msls' => array( array(), 'msls_the_switcher' ), + 'get_msls_flag_url' => array( array( 'en' ), 'msls_get_flag_url' ), + 'get_msls_blog_description' => array( array( 'en' ), 'msls_get_blog_description' ), + 'get_msls_permalink' => array( array( 'en' ), 'msls_get_permalink' ), ); + + $data = array(); + + foreach ( $legacy_functions as $legacy => list( $args, $replacement ) ) { + $data[ $legacy ] = array( $legacy, $args, $replacement ); + } + + return $data; } /**