From b7c8534ab1147b88bea50ace3969b51e3e31851f Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 14:16:02 +1000 Subject: [PATCH 01/14] Add GitHub Actions workflow to run all unit tests Adds CI for PHP standalone tests (Serve Happy, Browse Happy, Slack Trac Bot), WordPress-dependent tests (Handbook, Plugin Directory), and JavaScript tests (Pattern Directory theme) across pushes and PRs to trunk. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 131 +++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..6cc4def87e --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,131 @@ +name: Unit Tests + +on: + push: + branches: [ trunk ] + pull_request: + branches: [ trunk ] + +jobs: + # Standalone PHP tests — no WordPress or database dependency. + php-standalone: + name: "PHP: ${{ matrix.name }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Serve Happy API + working-directory: api.wordpress.org/public_html/core/serve-happy/1.0 + phpunit-args: "--configuration phpunit.xml --exclude-group serve-happy-live-http" + phpunit-version: "7" + php-version: "7.4" + - name: Browse Happy API + working-directory: api.wordpress.org/public_html/core/browse-happy/1.0 + phpunit-args: "--configuration phpunit.xml" + phpunit-version: "9" + php-version: "8.0" + - name: Slack Trac Bot + working-directory: common/includes/tests/slack/trac + phpunit-args: "bot.php" + phpunit-version: "7" + php-version: "7.4" + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: phpunit:${{ matrix.phpunit-version }} + + - name: Run PHPUnit + working-directory: ${{ matrix.working-directory }} + run: phpunit ${{ matrix.phpunit-args }} + + # WordPress-dependent PHP tests — require the WP test framework and MySQL. + php-wordpress: + name: "WP PHP: ${{ matrix.name }}" + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: wordpress_test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + fail-fast: false + matrix: + include: + - name: Handbook Plugin + working-directory: wordpress.org/public_html/wp-content/plugins/handbook + phpunit-args: "--configuration phpunit.xml.dist" + needs-bootstrap-wrapper: false + - name: Plugin Directory + working-directory: wordpress.org/public_html/wp-content/plugins/plugin-directory + phpunit-args: "--configuration phpunit.xml" + needs-bootstrap-wrapper: true + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.0" + extensions: mysqli + tools: phpunit:^9 + + - name: Install WordPress test suite + run: | + git clone --depth=1 https://github.com/WordPress/wordpress-develop.git /tmp/wp-develop + cp /tmp/wp-develop/wp-tests-config-sample.php /tmp/wp-develop/tests/phpunit/wp-tests-config.php + sed -i "s|dirname( __FILE__ ) . '/src/'|'/tmp/wp-develop/src/'|" /tmp/wp-develop/tests/phpunit/wp-tests-config.php + sed -i "s/youremptytestdbnamehere/wordpress_test/" /tmp/wp-develop/tests/phpunit/wp-tests-config.php + sed -i "s/yourusernamehere/root/" /tmp/wp-develop/tests/phpunit/wp-tests-config.php + sed -i "s/yourpasswordhere/root/" /tmp/wp-develop/tests/phpunit/wp-tests-config.php + sed -i "s/'localhost'/'127.0.0.1'/" /tmp/wp-develop/tests/phpunit/wp-tests-config.php + + - name: Create bootstrap wrapper for Plugin Directory + if: matrix.needs-bootstrap-wrapper + run: | + cat > /tmp/bootstrap-wrapper.php << 'PHPEOF' + Date: Thu, 12 Feb 2026 14:29:56 +1000 Subject: [PATCH 02/14] Fix CI: PHPUnit compatibility bootstrap and WP polyfills - Standardize all standalone PHP tests on PHP 8.0 + PHPUnit 9, with a compatibility bootstrap that aliases PHPUnit_Framework_TestCase for tests still using the pre-PHPUnit 8 class name (Serve Happy, Slack Trac Bot). - Run `composer install` in wordpress-develop to provide the yoast/phpunit-polyfills package required by the WP test suite. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6cc4def87e..1e7cb9b784 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,30 +18,43 @@ jobs: - name: Serve Happy API working-directory: api.wordpress.org/public_html/core/serve-happy/1.0 phpunit-args: "--configuration phpunit.xml --exclude-group serve-happy-live-http" - phpunit-version: "7" - php-version: "7.4" + needs-compat-bootstrap: true - name: Browse Happy API working-directory: api.wordpress.org/public_html/core/browse-happy/1.0 phpunit-args: "--configuration phpunit.xml" - phpunit-version: "9" - php-version: "8.0" + needs-compat-bootstrap: false - name: Slack Trac Bot working-directory: common/includes/tests/slack/trac phpunit-args: "bot.php" - phpunit-version: "7" - php-version: "7.4" + needs-compat-bootstrap: true steps: - uses: actions/checkout@v4 - - name: Set up PHP ${{ matrix.php-version }} + - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-version }} - tools: phpunit:${{ matrix.phpunit-version }} + php-version: "8.0" + tools: phpunit:^9 + + - name: Create PHPUnit compatibility bootstrap + if: matrix.needs-compat-bootstrap + run: | + cat > /tmp/phpunit-compat-bootstrap.php << 'PHPEOF' + Date: Thu, 12 Feb 2026 14:48:31 +1000 Subject: [PATCH 03/14] Fix CI: use PHP 7.4 for WP tests, drop Plugin Directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use PHP 7.4 for WordPress tests — the Handbook test setUp/tearDown methods lack `: void` return types, which is fatal on PHP 8.0+ with the Yoast PHPUnit Polyfills. - Remove Plugin Directory from CI — it has a hard dependency on the Jetpack_Search class which requires the full Jetpack plugin. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1e7cb9b784..8229e2cf60 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -76,18 +76,18 @@ jobs: - name: Handbook Plugin working-directory: wordpress.org/public_html/wp-content/plugins/handbook phpunit-args: "--configuration phpunit.xml.dist" - needs-bootstrap-wrapper: false - - name: Plugin Directory - working-directory: wordpress.org/public_html/wp-content/plugins/plugin-directory - phpunit-args: "--configuration phpunit.xml" - needs-bootstrap-wrapper: true + # Plugin Directory is excluded — it has a hard dependency on Jetpack + # (Jetpack_Search class) that cannot be satisfied without installing the + # full Jetpack plugin, which is not part of this repository. steps: - uses: actions/checkout@v4 - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.0" + # PHP 7.4: the Handbook tests lack `: void` return types on setUp/tearDown, + # which is fatal on PHP 8.0+ with the Yoast PHPUnit Polyfills. + php-version: "7.4" extensions: mysqli tools: phpunit:^9 @@ -102,27 +102,11 @@ jobs: sed -i "s/yourpasswordhere/root/" tests/phpunit/wp-tests-config.php sed -i "s/'localhost'/'127.0.0.1'/" tests/phpunit/wp-tests-config.php - - name: Create bootstrap wrapper for Plugin Directory - if: matrix.needs-bootstrap-wrapper - run: | - cat > /tmp/bootstrap-wrapper.php << 'PHPEOF' - Date: Thu, 12 Feb 2026 17:41:09 +1000 Subject: [PATCH 04/14] Fix Handbook tests, remove JS job - Add `: void` return types to setUp/tearDown in Handbook test files to be compatible with the Yoast PHPUnit Polyfills. - Restore PHP 8.0 for WordPress tests (no longer need 7.4 workaround). - Remove JS Pattern Directory Theme job per request. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 24 +------------------ .../handbook/phpunit/tests/handbook.php | 4 ++-- .../plugins/handbook/phpunit/tests/init.php | 4 ++-- .../handbook/phpunit/tests/template-tags.php | 4 ++-- 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 8229e2cf60..3ba521af6c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -85,9 +85,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - # PHP 7.4: the Handbook tests lack `: void` return types on setUp/tearDown, - # which is fatal on PHP 8.0+ with the Yoast PHPUnit Polyfills. - php-version: "7.4" + php-version: "8.0" extensions: mysqli tools: phpunit:^9 @@ -107,23 +105,3 @@ jobs: env: WP_TESTS_DIR: /tmp/wp-develop/tests/phpunit run: phpunit ${{ matrix.phpunit-args }} - - # JavaScript unit tests. - js-tests: - name: "JS: Pattern Directory Theme" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install dependencies - working-directory: wordpress.org/public_html/wp-content/themes/pub/wporg-patterns - run: npm install - - - name: Run unit tests - working-directory: wordpress.org/public_html/wp-content/themes/pub/wporg-patterns - run: npm run test:unit diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php index 9199c54ccc..40ad4cec01 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/handbook.php @@ -17,7 +17,7 @@ class WPorg_Handbook_Handbook_Test extends WP_UnitTestCase { protected $handbook; - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); @@ -25,7 +25,7 @@ public function setUp() { $this->handbook = reset( $handbooks ); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); foreach ( WPorg_Handbook_Init::get_handbook_objects() as $obj ) { diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php index 1e1fd2f050..9b03669507 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/init.php @@ -4,13 +4,13 @@ class WPorg_Handbook_Init_Test extends WP_UnitTestCase { - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); WPorg_Handbook_Init::reset( true ); diff --git a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php index aa9b6d91b5..acf9dbad22 100644 --- a/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php +++ b/wordpress.org/public_html/wp-content/plugins/handbook/phpunit/tests/template-tags.php @@ -4,13 +4,13 @@ class WPorg_Handbook_Template_Tags_Test extends WP_UnitTestCase { - public function setUp() { + public function setUp(): void { parent::setup(); WPorg_Handbook_Init::init(); } - public function tearDown() { + public function tearDown(): void { parent::tearDown(); WPorg_Handbook_Init::reset( true ); From 6c91f07010ed561587c8e80e097670416ede865e Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 17:42:49 +1000 Subject: [PATCH 05/14] Fix Browse Happy test: account for IE always needing upgrade The parse logic unconditionally sets upgrade=true for Internet Explorer (since it is discontinued), but the test did not account for this and expected upgrade=false when the IE version matched the current version. Co-Authored-By: Claude Opus 4.6 --- .../browse-happy/1.0/tests/phpunit/tests/browse-happy.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.wordpress.org/public_html/core/browse-happy/1.0/tests/phpunit/tests/browse-happy.php b/api.wordpress.org/public_html/core/browse-happy/1.0/tests/phpunit/tests/browse-happy.php index 221ca814c8..eaa3002c25 100644 --- a/api.wordpress.org/public_html/core/browse-happy/1.0/tests/phpunit/tests/browse-happy.php +++ b/api.wordpress.org/public_html/core/browse-happy/1.0/tests/phpunit/tests/browse-happy.php @@ -853,6 +853,12 @@ function test_upgrade_browsers( $header ) { return; } + // Internet Explorer is always flagged as needing an upgrade. + if ( 'Internet Explorer' === $parsed['name'] ) { + $this->assertTrue( $parsed['upgrade'] ); + return; + } + $versions = get_browser_current_versions(); if ( ! empty( $versions[ $parsed['name'] ] ) ) { From c2fb2b194c23c68420fd0e3f42edfd3c7f555325 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 17:54:59 +1000 Subject: [PATCH 06/14] Add Slack Props and Plugin Directory tests, fix Handbook sidebar assertion - Add Slack Props Library to standalone PHP tests with a wpdbStub bootstrap so PHPUnit can mock the database dependency. - Re-add Plugin Directory to WP tests with a bootstrap that stubs Jetpack Search classes (Classic_Search) so the plugin can load without the full Jetpack plugin installed. - Fix Handbook test_handbook_sidebar: add show_in_rest to the expected sidebar keys array (added in WordPress trunk). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 72 ++++++++++++++++--- .../handbook/phpunit/tests/handbook.php | 2 +- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 3ba521af6c..0480c36a6f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,15 +18,19 @@ jobs: - name: Serve Happy API working-directory: api.wordpress.org/public_html/core/serve-happy/1.0 phpunit-args: "--configuration phpunit.xml --exclude-group serve-happy-live-http" - needs-compat-bootstrap: true + bootstrap: compat - name: Browse Happy API working-directory: api.wordpress.org/public_html/core/browse-happy/1.0 phpunit-args: "--configuration phpunit.xml" - needs-compat-bootstrap: false + bootstrap: none - name: Slack Trac Bot working-directory: common/includes/tests/slack/trac phpunit-args: "bot.php" - needs-compat-bootstrap: true + bootstrap: compat + - name: Slack Props Library + working-directory: common/includes/slack/props/tests + phpunit-args: "test-lib.php" + bootstrap: wpdb-stub steps: - uses: actions/checkout@v4 @@ -37,9 +41,9 @@ jobs: tools: phpunit:^9 - name: Create PHPUnit compatibility bootstrap - if: matrix.needs-compat-bootstrap + if: matrix.bootstrap == 'compat' run: | - cat > /tmp/phpunit-compat-bootstrap.php << 'PHPEOF' + cat > /tmp/phpunit-bootstrap.php << 'PHPEOF' /tmp/phpunit-bootstrap.php << 'PHPEOF' + /tmp/bootstrap-wrapper.php << 'PHPEOF' + assertTrue( isset( $wp_registered_sidebars[ 'handbook' ] ) ); $this->assertSame( - [ 'name', 'id', 'description', 'class', 'before_widget', 'after_widget', 'before_title', 'after_title', 'before_sidebar', 'after_sidebar' ], + [ 'name', 'id', 'description', 'class', 'before_widget', 'after_widget', 'before_title', 'after_title', 'before_sidebar', 'after_sidebar', 'show_in_rest' ], array_keys( $wp_registered_sidebars[ 'handbook' ] ) ); $this->assertEquals( 'handbook', $wp_registered_sidebars[ 'handbook' ]['id'] ); From f0b924d6f0f5716ca2fc8648eb83087831dff538 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 18:06:36 +1000 Subject: [PATCH 07/14] Add wp-env support for Handbook and Plugin Directory - Handbook: Add wp-env test directory fallback to bootstrap - Plugin Directory: Create .wp-env.json with Jetpack search stub mu-plugin mapping - Plugin Directory: Make bootstrap self-contained (finds WP test framework, loads Jetpack stub, boots WP) - Plugin Directory: Add void return types to setUp/tearDown - CI: Remove Jetpack stub CI step (now handled by bootstrap) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 36 +------------------ .../plugins/handbook/phpunit/bootstrap.php | 4 +++ .../plugins/plugin-directory/.wp-env.json | 7 ++++ .../plugin-directory/tests/bootstrap.php | 26 ++++++++++++++ .../tests/wporg-plugin-api-performance.php | 6 ++-- .../tests/phpunit/tests/wporg-plugin-api.php | 4 +-- .../tests/stubs/jetpack-search-stub.php | 23 ++++++++++++ 7 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0480c36a6f..0e5c3c1342 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -92,11 +92,9 @@ jobs: - name: Handbook Plugin working-directory: wordpress.org/public_html/wp-content/plugins/handbook phpunit-args: "--configuration phpunit.xml.dist" - needs-custom-bootstrap: false - name: Plugin Directory working-directory: wordpress.org/public_html/wp-content/plugins/plugin-directory phpunit-args: "--configuration phpunit.xml" - needs-custom-bootstrap: true steps: - uses: actions/checkout@v4 @@ -118,40 +116,8 @@ jobs: sed -i "s/yourpasswordhere/root/" tests/phpunit/wp-tests-config.php sed -i "s/'localhost'/'127.0.0.1'/" tests/phpunit/wp-tests-config.php - - name: Create Plugin Directory bootstrap - if: matrix.needs-custom-bootstrap - run: | - cat > /tmp/bootstrap-wrapper.php << 'PHPEOF' - true, ); - function setUp() { + function setUp(): void { parent::setUp(); add_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) ); } - function tearDown() { + function tearDown(): void { remove_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) ); parent::tearDown(); } @@ -64,7 +64,7 @@ static function averages( $values, $decimals = 4 ) { } - static function tearDownAfterClass() { + static function tearDownAfterClass(): void { global $wporg_plugin_api_performance; echo 'Performance summary for ' . get_called_class() . ":\n"; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php index 7f04e61be3..4d837d6336 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/wporg-plugin-api.php @@ -44,12 +44,12 @@ class Tests_Plugins_API extends WP_UnitTestCase { 'donate_link' => true, ); - function setUp() { + function setUp(): void { parent::setUp(); add_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) ); } - function tearDown() { + function tearDown(): void { remove_filter( 'http_headers_useragent', array( $this, 'filter_http_headers_useragent' ) ); parent::tearDown(); } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php new file mode 100644 index 0000000000..81b600a18d --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php @@ -0,0 +1,23 @@ + Date: Thu, 12 Feb 2026 18:48:58 +1000 Subject: [PATCH 08/14] Fix CI: Slack Props filename issue, remove Plugin Directory integration tests - Slack Props: Pass directory instead of hyphenated filename to PHPUnit 9 (PHPUnit 9 cannot derive class name from hyphenated filenames) - Plugin Directory: Remove from CI as all tests are integration tests that require live api.wordpress.org (wp-env config still available for local development) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0e5c3c1342..592245edc9 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -29,7 +29,7 @@ jobs: bootstrap: compat - name: Slack Props Library working-directory: common/includes/slack/props/tests - phpunit-args: "test-lib.php" + phpunit-args: "." bootstrap: wpdb-stub steps: - uses: actions/checkout@v4 @@ -92,9 +92,6 @@ jobs: - name: Handbook Plugin working-directory: wordpress.org/public_html/wp-content/plugins/handbook phpunit-args: "--configuration phpunit.xml.dist" - - name: Plugin Directory - working-directory: wordpress.org/public_html/wp-content/plugins/plugin-directory - phpunit-args: "--configuration phpunit.xml" steps: - uses: actions/checkout@v4 From 438aa6dd0c49a8bc71f13befacc6051763ab7921 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 18:52:42 +1000 Subject: [PATCH 09/14] Enhance Plugin Directory wp-env with production dependencies - Add Jetpack, advanced-post-cache, and plugin-check as plugins - Add wporg-plugins-2024 theme and wporg-parent-2021 parent theme - Add WPORG_Ratings stub mu-plugin (closed-source dependency) - Document all code locations that rely on each stub Co-Authored-By: Claude Opus 4.6 --- .../plugins/plugin-directory/.wp-env.json | 13 +++++-- .../tests/stubs/jetpack-search-stub.php | 7 ++++ .../tests/stubs/wporg-ratings-stub.php | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json b/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json index 3d80f275fa..970c05e2a2 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/.wp-env.json @@ -1,7 +1,16 @@ { "core": "WordPress/WordPress#master", - "plugins": [ "." ], + "plugins": [ + ".", + "https://downloads.wordpress.org/plugin/jetpack.latest-stable.zip", + "https://downloads.wordpress.org/plugin/advanced-post-cache.latest-stable.zip", + "https://github.com/WordPress/plugin-check" + ], + "themes": [ + "../../../themes/pub/wporg-plugins-2024", + "https://github.com/WordPress/wporg-parent-2021" + ], "mappings": { - "wp-content/mu-plugins/jetpack-search-stub.php": "./tests/stubs/jetpack-search-stub.php" + "wp-content/mu-plugins/wporg-ratings-stub.php": "./tests/stubs/wporg-ratings-stub.php" } } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php index 81b600a18d..175877e2c6 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php @@ -2,6 +2,13 @@ /** * Plugin Name: Jetpack Search Stub * Description: Provides stub classes for the Jetpack Search dependency used by Plugin Directory. + * + * The plugin-directory plugin references Jetpack Search in the following location: + * + * - class-plugin-search.php:78-92 — Plugin_Search::init() loads Jetpack autoloader, + * then checks for \Automattic\Jetpack\Search\Classic_Search (new Jetpack) or falls + * back to \Jetpack_Search (old Jetpack). Without either class, the else branch fatals. + * This is called during the 'init' action when Plugin_Search is instantiated. */ namespace Automattic\Jetpack\Search; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php new file mode 100644 index 0000000000..e8fb8eb147 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/wporg-ratings-stub.php @@ -0,0 +1,34 @@ + Date: Thu, 12 Feb 2026 18:59:55 +1000 Subject: [PATCH 10/14] Add Plugin Directory unit tests for core components New test suites covering: - Template helpers: sanitize_active_installs, format_active_installs, section titles, version detection, encoding - Trademarks: slug validation, prefix detection, for-woocommerce exception, portmanteau detection, user/published-plugin exceptions - Readme Parser: header parsing, section parsing, FAQ extraction, alias sections, ignored tags, short description truncation - Markdown: transform, equals-style headers, code blocks, lists Tests use @group plugin-directory to run separately from legacy integration tests that require live api.wordpress.org. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/unit-tests.yml | 3 + .../tests/phpunit/tests/test-markdown.php | 73 ++++++ .../phpunit/tests/test-readme-parser.php | 232 ++++++++++++++++++ .../tests/phpunit/tests/test-template.php | 107 ++++++++ .../tests/phpunit/tests/test-trademarks.php | 119 +++++++++ 5 files changed, 534 insertions(+) create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 592245edc9..20475ee617 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -92,6 +92,9 @@ jobs: - name: Handbook Plugin working-directory: wordpress.org/public_html/wp-content/plugins/handbook phpunit-args: "--configuration phpunit.xml.dist" + - name: Plugin Directory + working-directory: wordpress.org/public_html/wp-content/plugins/plugin-directory + phpunit-args: "--configuration phpunit.xml --group plugin-directory" steps: - uses: actions/checkout@v4 diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php new file mode 100644 index 0000000000..b547adb9c6 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php @@ -0,0 +1,73 @@ +transform( 'Hello World' ); + $this->assertSame( '

Hello World

', $result ); + } + + function test_transform_bold(): void { + $md = new Markdown(); + $result = $md->transform( '**bold text**' ); + $this->assertStringContainsString( 'bold text', $result ); + } + + function test_transform_italic(): void { + $md = new Markdown(); + $result = $md->transform( '*italic text*' ); + $this->assertStringContainsString( 'italic text', $result ); + } + + function test_transform_link(): void { + $md = new Markdown(); + $result = $md->transform( '[Example](https://example.com)' ); + $this->assertStringContainsString( 'Example', $result ); + } + + function test_transform_equals_header(): void { + $md = new Markdown(); + $result = $md->transform( '= Section Title =' ); + $this->assertStringContainsString( '

Section Title

', $result ); + } + + function test_transform_code_block(): void { + $md = new Markdown(); + $result = $md->transform( "```\ncode here\n```" ); + $this->assertStringContainsString( '', $result ); + } + + function test_transform_unordered_list(): void { + $md = new Markdown(); + $result = $md->transform( "* Item 1\n* Item 2\n* Item 3" ); + $this->assertStringContainsString( '
  • Item 1
  • ', $result ); + $this->assertStringContainsString( '
  • Item 2
  • ', $result ); + $this->assertStringContainsString( '
      ', $result ); + } + + function test_transform_trims_output(): void { + $md = new Markdown(); + $result = $md->transform( " \n Hello \n " ); + $this->assertSame( $result, trim( $result ) ); + } + + function test_transform_empty_string(): void { + $md = new Markdown(); + $result = $md->transform( '' ); + $this->assertSame( '', $result ); + } + + function test_transform_multiple_equals_headers(): void { + $md = new Markdown(); + $result = $md->transform( "= First =\n\nContent\n\n= Second =" ); + $this->assertStringContainsString( '

      First

      ', $result ); + $this->assertStringContainsString( '

      Second

      ', $result ); + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php new file mode 100644 index 0000000000..d3dd85367c --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php @@ -0,0 +1,232 @@ +assertSame( 'My Test Plugin', $parser->name ); + } + + function test_parses_contributors(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( [ 'johndoe' ], $parser->contributors ); + } + + function test_parses_tags(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( [ 'test', 'unit-test' ], $parser->tags ); + } + + function test_parses_requires(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( '5.0', $parser->requires ); + } + + function test_parses_tested(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( '6.4', $parser->tested ); + } + + function test_parses_stable_tag(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( '1.2.3', $parser->stable_tag ); + } + + function test_parses_requires_php(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( '7.4', $parser->requires_php ); + } + + function test_parses_short_description(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( 'A short description of the plugin.', $parser->short_description ); + } + + function test_parses_license(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertSame( 'GPLv2', $parser->license ); + } + + function test_parses_sections(): void { + $parser = new Parser( self::$valid_readme ); + + $this->assertArrayHasKey( 'description', $parser->sections ); + $this->assertArrayHasKey( 'installation', $parser->sections ); + $this->assertArrayHasKey( 'faq', $parser->sections ); + $this->assertArrayHasKey( 'changelog', $parser->sections ); + } + + function test_description_section_content(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertStringContainsString( 'longer description', $parser->sections['description'] ); + } + + function test_faq_parsed(): void { + $parser = new Parser( self::$valid_readme ); + $this->assertNotEmpty( $parser->faq ); + $this->assertArrayHasKey( 'How does it work?', $parser->faq ); + } + + function test_empty_readme_produces_warnings(): void { + $parser = new Parser( '' ); + $this->assertEmpty( $parser->name ); + } + + function test_readme_without_name_header(): void { + $readme = <<<'README' +Contributors: johndoe +Tags: test +Requires at least: 5.0 +Tested up to: 6.0 +Stable tag: 1.0 + +Short description. + +== Description == + +Long description. +README; + $parser = new Parser( $readme ); + $this->assertArrayHasKey( 'invalid_plugin_name_header', $parser->warnings ); + } + + function test_readme_alias_sections(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Stable tag: 1.0 + +Short description. + +== Frequently Asked Questions == + += Question? = + +Answer. + +== Change Log == + += 1.0 = +* Initial release. +README; + $parser = new Parser( $readme ); + + // 'frequently_asked_questions' is aliased to 'faq'. + $this->assertArrayHasKey( 'faq', $parser->sections ); + + // 'change_log' is aliased to 'changelog'. + $this->assertArrayHasKey( 'changelog', $parser->sections ); + } + + function test_ignored_tags_filtered(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tags: plugin, wordpress, seo, test +Stable tag: 1.0 + +Short description. + +== Description == + +A plugin. +README; + $parser = new Parser( $readme ); + + // 'plugin' and 'wordpress' should be filtered out. + $this->assertNotContains( 'plugin', $parser->tags ); + $this->assertNotContains( 'wordpress', $parser->tags ); + $this->assertContains( 'seo', $parser->tags ); + $this->assertContains( 'test', $parser->tags ); + } + + function test_valid_headers_mapping(): void { + // 'tested up to' and 'tested' both map to 'tested'. + $readme_tested_up_to = <<<'README' +=== Test === +Contributors: johndoe +Tested up to: 6.5 +Stable tag: 1.0 + +Short desc. +README; + $parser = new Parser( $readme_tested_up_to ); + $this->assertSame( '6.5', $parser->tested ); + + // 'requires at least' maps to 'requires'. + $readme_requires = <<<'README' +=== Test === +Contributors: johndoe +Requires at least: 5.5 +Stable tag: 1.0 + +Short desc. +README; + $parser = new Parser( $readme_requires ); + $this->assertSame( '5.5', $parser->requires ); + } + + function test_short_description_is_truncated(): void { + $long_desc = str_repeat( 'a', 200 ); + $readme = "=== Test ===\nContributors: johndoe\nStable tag: 1.0\n\n{$long_desc}\n\n== Description ==\n\nDesc."; + + $parser = new Parser( $readme ); + $this->assertLessThanOrEqual( 150, strlen( $parser->short_description ) ); + } + + function test_donate_link_parsed(): void { + $readme = <<<'README' +=== Test === +Contributors: johndoe +Donate link: https://example.com/donate +Stable tag: 1.0 + +Short desc. + +== Description == + +Desc. +README; + $parser = new Parser( $readme ); + $this->assertSame( 'https://example.com/donate', $parser->donate_link ); + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php new file mode 100644 index 0000000000..404e0d60f1 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php @@ -0,0 +1,107 @@ +assertSame( $expected, Template::sanitize_active_installs( $input ) ); + } + + function data_sanitize_active_installs(): array { + return [ + 'zero' => [ 0, 0 ], + 'single digit' => [ 5, 0 ], + 'ten' => [ 10, 10 ], + 'twelve' => [ 12, 10 ], + 'ninety nine' => [ 99, 90 ], + 'one hundred' => [ 100, 100 ], + 'hundred fifty' => [ 150, 100 ], + 'nine ninety nine' => [ 999, 900 ], + 'one thousand' => [ 1000, 1000 ], + 'one thousand five' => [ 1500, 1000 ], + 'ten thousand' => [ 10000, 10000 ], + 'fifteen thousand' => [ 15000, 10000 ], + 'hundred thousand' => [ 100000, 100000 ], + 'five hundred k' => [ 500000, 500000 ], + 'one million' => [ 1000000, 1000000 ], + 'five million' => [ 5000000, 5000000 ], + 'ten million' => [ 10000000, 10000000 ], + 'over ten million' => [ 15000000, 10000000 ], + 'fifty million' => [ 50000000, 10000000 ], + ]; + } + + /** + * @dataProvider data_format_active_installs_for_display + */ + function test_format_active_installs_for_display( $input, $expected ): void { + $this->assertSame( $expected, Template::format_active_installs_for_display( $input ) ); + } + + function data_format_active_installs_for_display(): array { + return [ + 'zero' => [ 0, 'Fewer than 10' ], + 'five' => [ 5, 'Fewer than 10' ], + 'nine' => [ 9, 'Fewer than 10' ], + 'ten' => [ 10, '10+' ], + 'hundred' => [ 100, '100+' ], + 'thousand' => [ 1000, '1,000+' ], + 'ten thousand' => [ 10000, '10,000+' ], + 'one million' => [ 1000000, '1+ million' ], + 'two million' => [ 2000000, '2+ million' ], + 'ten million' => [ 10000000, '10+ million' ], + ]; + } + + function test_get_plugin_section_titles(): void { + $titles = Template::get_plugin_section_titles(); + + $this->assertIsArray( $titles ); + + $expected_keys = [ + 'description', + 'installation', + 'faq', + 'screenshots', + 'changelog', + 'stats', + 'support', + 'reviews', + 'developers', + 'other_notes', + 'blocks', + ]; + + $this->assertSame( $expected_keys, array_keys( $titles ) ); + } + + function test_get_current_major_wp_version_returns_float(): void { + $version = Template::get_current_major_wp_version(); + + $this->assertIsFloat( $version ); + $this->assertGreaterThan( 0, $version ); + } + + /** + * @dataProvider data_encode + */ + function test_encode( $input, $expected ): void { + $this->assertSame( $expected, Template::encode( $input ) ); + } + + function data_encode(): array { + return [ + 'plain ascii' => [ 'Hello World', 'Hello World' ], + 'empty string' => [ '', '' ], + 'utf8 chars' => [ 'café', 'café' ], + ]; + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php new file mode 100644 index 0000000000..03d1dcd874 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php @@ -0,0 +1,119 @@ +assertFalse( Trademarks::check_slug( $slug ) ); + } + + function data_clean_slugs(): array { + return [ + 'generic slug' => [ 'my-cool-plugin' ], + 'simple slug' => [ 'hello-world' ], + 'numeric slug' => [ 'plugin-2024' ], + 'short slug' => [ 'foo' ], + ]; + } + + /** + * Test that trademarked prefix slugs are detected. + * + * @dataProvider data_trademarked_prefix_slugs + */ + function test_trademarked_prefix_slugs( $slug, $expected_trademark ): void { + $result = Trademarks::check_slug( $slug ); + $this->assertIsArray( $result ); + $this->assertContains( $expected_trademark, $result ); + } + + function data_trademarked_prefix_slugs(): array { + return [ + 'google prefix' => [ 'google-analytics-tool', 'google-' ], + 'facebook in slug' => [ 'my-facebook-share', 'facebook' ], + 'jetpack prefix' => [ 'jetpack-addon', 'jetpack-' ], + 'stripe prefix' => [ 'stripe-payments', 'stripe-' ], + 'paypal prefix' => [ 'paypal-checkout', 'paypal-' ], + 'woocommerce slug' => [ 'woocommerce-extras', 'woocommerce' ], + 'instagram slug' => [ 'instagram-feed-widget', 'instagram' ], + 'twitter slug' => [ 'twitter-cards', 'twitter-' ], + 'chatgpt prefix' => [ 'chatgpt-assistant', 'chatgpt-' ], + ]; + } + + /** + * Test the for-woocommerce exception. + */ + function test_for_woocommerce_exception(): void { + // "something-for-woocommerce" is allowed. + $this->assertFalse( Trademarks::check_slug( 'my-payments-for-woocommerce' ) ); + + // But "woocommerce-something" is not. + $result = Trademarks::check_slug( 'woocommerce-payments' ); + $this->assertIsArray( $result ); + } + + /** + * Test that check() converts a plugin name to slug first. + */ + function test_check_converts_name_to_slug(): void { + // "My Cool Plugin" should be clean. + $this->assertFalse( Trademarks::check( 'My Cool Plugin' ) ); + + // "Google Maps Plugin" should be trademarked. + $result = Trademarks::check( 'Google Maps Plugin' ); + $this->assertIsArray( $result ); + } + + /** + * Test that trademark exceptions work. + */ + function test_trademark_exceptions(): void { + // Without exception, jetpack- is trademarked. + $result = Trademarks::check_slug( 'jetpack-boost-extra' ); + $this->assertIsArray( $result ); + + // With automattic.com exception, jetpack- is allowed. + $result = Trademarks::check_slug( 'jetpack-boost-extra', [ 'automattic.com' ] ); + $this->assertFalse( $result ); + } + + /** + * Test published plugin exception for wp- prefix. + */ + function test_published_plugin_wp_prefix_exception(): void { + // wp- is flagged for new plugins. + $result = Trademarks::check_slug( 'wp-super-cache' ); + $this->assertIsArray( $result ); + + // published-plugin exception allows wp-. + $result = Trademarks::check_slug( 'wp-super-cache', [ 'published-plugin' ] ); + $this->assertFalse( $result ); + } + + /** + * Test portmanteau detection (e.g. woopress). + */ + function test_portmanteau_detection(): void { + $result = Trademarks::check_slug( 'woopress' ); + $this->assertIsArray( $result ); + } + + /** + * Test that the trademarked slugs list is not empty. + */ + function test_trademarked_slugs_list_is_populated(): void { + $this->assertNotEmpty( Trademarks::$trademarked_slugs ); + $this->assertGreaterThan( 50, count( Trademarks::$trademarked_slugs ) ); + } +} From 146b98032e6a30183e4ecbf05325918912b2e831 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 19:02:01 +1000 Subject: [PATCH 11/14] Fix Plugin Directory test assertions - Template: use assertEquals for floor() float return values - Trademarks: avoid 'plugin' (a trademarked slug) in clean slug tests Co-Authored-By: Claude Opus 4.6 --- .../tests/phpunit/tests/test-template.php | 3 ++- .../tests/phpunit/tests/test-trademarks.php | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php index 404e0d60f1..de2ce81aed 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php @@ -12,7 +12,8 @@ class Tests_Template extends WP_UnitTestCase { * @dataProvider data_sanitize_active_installs */ function test_sanitize_active_installs( $input, $expected ): void { - $this->assertSame( $expected, Template::sanitize_active_installs( $input ) ); + // floor() returns float, so use assertEquals for loose type comparison. + $this->assertEquals( $expected, Template::sanitize_active_installs( $input ) ); } function data_sanitize_active_installs(): array { diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php index 03d1dcd874..f7904bf61f 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-trademarks.php @@ -18,10 +18,11 @@ function test_clean_slugs_pass( $slug ): void { } function data_clean_slugs(): array { + // Note: 'plugin' is a trademarked slug, so avoid it in clean slugs. return [ - 'generic slug' => [ 'my-cool-plugin' ], + 'generic slug' => [ 'my-cool-tool' ], 'simple slug' => [ 'hello-world' ], - 'numeric slug' => [ 'plugin-2024' ], + 'numeric slug' => [ 'acme-2024' ], 'short slug' => [ 'foo' ], ]; } @@ -67,11 +68,11 @@ function test_for_woocommerce_exception(): void { * Test that check() converts a plugin name to slug first. */ function test_check_converts_name_to_slug(): void { - // "My Cool Plugin" should be clean. - $this->assertFalse( Trademarks::check( 'My Cool Plugin' ) ); + // "My Cool Tool" should be clean (note: 'plugin' is trademarked). + $this->assertFalse( Trademarks::check( 'My Cool Tool' ) ); - // "Google Maps Plugin" should be trademarked. - $result = Trademarks::check( 'Google Maps Plugin' ); + // "Google Maps Helper" should be trademarked (contains 'google-'). + $result = Trademarks::check( 'Google Maps Helper' ); $this->assertIsArray( $result ); } From 1403817adcd7146e3568c7c7c79177d6d9f6f0ea Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 12 Feb 2026 19:04:38 +1000 Subject: [PATCH 12/14] Fix readme parser tests: contributors need WP user, truncation uses warning - Create a WP test user for contributor parsing test - Add separate test for invalid contributor warning - Check truncation warning instead of byte length (trim_length appends a hellip entity) Co-Authored-By: Claude Opus 4.6 --- .../tests/phpunit/tests/test-readme-parser.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php index d3dd85367c..49f0124acc 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-parser.php @@ -47,10 +47,21 @@ function test_parses_plugin_name(): void { } function test_parses_contributors(): void { + // Create a user so the contributor sanitization finds them. + self::factory()->user->create( [ 'user_login' => 'johndoe', 'user_nicename' => 'johndoe' ] ); + $parser = new Parser( self::$valid_readme ); $this->assertSame( [ 'johndoe' ], $parser->contributors ); } + function test_invalid_contributor_produces_warning(): void { + $parser = new Parser( self::$valid_readme ); + + // 'johndoe' does not exist as a WP user, so it should be ignored with a warning. + $this->assertEmpty( $parser->contributors ); + $this->assertArrayHasKey( 'contributor_ignored', $parser->warnings ); + } + function test_parses_tags(): void { $parser = new Parser( self::$valid_readme ); $this->assertSame( [ 'test', 'unit-test' ], $parser->tags ); @@ -205,12 +216,12 @@ function test_valid_headers_mapping(): void { $this->assertSame( '5.5', $parser->requires ); } - function test_short_description_is_truncated(): void { - $long_desc = str_repeat( 'a', 200 ); + function test_long_short_description_produces_warning(): void { + $long_desc = str_repeat( 'a ', 100 ); // 200 chars. $readme = "=== Test ===\nContributors: johndoe\nStable tag: 1.0\n\n{$long_desc}\n\n== Description ==\n\nDesc."; $parser = new Parser( $readme ); - $this->assertLessThanOrEqual( 150, strlen( $parser->short_description ) ); + $this->assertArrayHasKey( 'trimmed_short_description', $parser->warnings ); } function test_donate_link_parsed(): void { From 85bcff3430b5eca9ed9313a8525e985cd6248b2c Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 13 Feb 2026 10:39:21 +1000 Subject: [PATCH 13/14] Improve Plugin Directory test coverage and fix review issues - Add get_last_query_info() to Jetpack Search stub (called at class-plugin-search.php:437) - Improve Template::encode() tests with real UTF-8 and Windows-1252 encoding inputs - Add tests for dashicons_stars, close/rejection reasons, download_link, support URLs, geopattern icon URLs, is_plugin_outdated, rollout strategies - Refocus Markdown tests on custom code_trick() logic (pre/code block preservation, underscore handling, bbPress-style backtick blocks) - Add Readme Validator test file covering validate(), validate_content(), translate_code_to_message(), and all error/warning/note categories - Add Plugin Directory registration tests for post type, taxonomies, post statuses, meta fields, and filter_wp_insert_post_data Co-Authored-By: Claude Opus 4.6 --- .../tests/phpunit/tests/test-markdown.php | 127 ++++-- .../phpunit/tests/test-plugin-directory.php | 253 ++++++++++++ .../phpunit/tests/test-readme-validator.php | 360 ++++++++++++++++++ .../tests/phpunit/tests/test-template.php | 175 ++++++++- .../tests/stubs/jetpack-search-stub.php | 12 + 5 files changed, 895 insertions(+), 32 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php index b547adb9c6..1527c8e391 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php @@ -14,60 +14,129 @@ function test_transform_basic_paragraph(): void { $this->assertSame( '

      Hello World

      ', $result ); } - function test_transform_bold(): void { - $md = new Markdown(); - $result = $md->transform( '**bold text**' ); - $this->assertStringContainsString( 'bold text', $result ); - } - - function test_transform_italic(): void { + function test_transform_empty_string(): void { $md = new Markdown(); - $result = $md->transform( '*italic text*' ); - $this->assertStringContainsString( 'italic text', $result ); + $result = $md->transform( '' ); + $this->assertSame( '', $result ); } - function test_transform_link(): void { + function test_transform_trims_output(): void { $md = new Markdown(); - $result = $md->transform( '[Example](https://example.com)' ); - $this->assertStringContainsString( 'Example', $result ); + $result = $md->transform( " \n Hello \n " ); + $this->assertSame( $result, trim( $result ) ); } + /** + * Test the custom `= Section Title =` header syntax. + * + * This is WordPress plugin readme specific — converts `= Title =` to

      . + */ function test_transform_equals_header(): void { $md = new Markdown(); $result = $md->transform( '= Section Title =' ); $this->assertStringContainsString( '

      Section Title

      ', $result ); } - function test_transform_code_block(): void { + function test_transform_multiple_equals_headers(): void { $md = new Markdown(); - $result = $md->transform( "```\ncode here\n```" ); + $result = $md->transform( "= First =\n\nContent\n\n= Second =" ); + $this->assertStringContainsString( '

      First

      ', $result ); + $this->assertStringContainsString( '

      Second

      ', $result ); + } + + function test_equals_header_with_leading_whitespace(): void { + $md = new Markdown(); + $result = $md->transform( ' = Indented Header =' ); + $this->assertStringContainsString( '

      Indented Header

      ', $result ); + } + + /** + * Test code_trick:
       blocks are converted to markdown backtick format.
      +	 *
      +	 * This is custom logic in Markdown::code_trick() — pre-existing HTML code blocks
      +	 * are converted to backtick format before markdown processing, so markdown
      +	 * does not mangle underscores and other special characters inside code.
      +	 */
      +	function test_code_trick_pre_code_block(): void {
      +		$md     = new Markdown();
      +		$input  = "
      function my_func() { return true; }
      "; + $result = $md->transform( $input ); + + // The code should be preserved inside a code block. + $this->assertStringContainsString( 'my_func', $result ); $this->assertStringContainsString( '', $result ); } - function test_transform_unordered_list(): void { + function test_code_trick_preserves_underscores_in_pre_code(): void { $md = new Markdown(); - $result = $md->transform( "* Item 1\n* Item 2\n* Item 3" ); - $this->assertStringContainsString( '
    • Item 1
    • ', $result ); - $this->assertStringContainsString( '
    • Item 2
    • ', $result ); - $this->assertStringContainsString( '
        ', $result ); + $input = "
        \$my_var = some_function();
        "; + $result = $md->transform( $input ); + + // Underscores should NOT be converted to tags inside code blocks. + $this->assertStringNotContainsString( '', $result ); + $this->assertStringContainsString( 'my_var', $result ); + $this->assertStringContainsString( 'some_function', $result ); } - function test_transform_trims_output(): void { + function test_code_trick_inline_code(): void { $md = new Markdown(); - $result = $md->transform( " \n Hello \n " ); - $this->assertSame( $result, trim( $result ) ); + $input = "my_var_name"; + $result = $md->transform( $input ); + + // Inline code should also preserve underscores. + $this->assertStringNotContainsString( '', $result ); + $this->assertStringContainsString( 'my_var_name', $result ); } - function test_transform_empty_string(): void { + function test_code_trick_html_entities_decoded(): void { $md = new Markdown(); - $result = $md->transform( '' ); - $this->assertSame( '', $result ); + // HTML entities inside
         should be decoded back before markdown processing.
        +		$input  = "
        <div class="test">
        "; + $result = $md->transform( $input ); + + $this->assertStringContainsString( '', $result ); } - function test_transform_multiple_equals_headers(): void { + /** + * Test code_trick: bbPress-style backtick code blocks are converted to + * indented code (4 spaces) for markdown processing. + */ + function test_code_trick_bbpress_backtick_block(): void { $md = new Markdown(); - $result = $md->transform( "= First =\n\nContent\n\n= Second =" ); - $this->assertStringContainsString( '

        First

        ', $result ); - $this->assertStringContainsString( '

        Second

        ', $result ); + // bbPress-style block-level backtick code. + $input = "\n`some_code_here`"; + $result = $md->transform( $input ); + + // Should be rendered as a code block, not inline code. + $this->assertStringContainsString( 'some_code_here', $result ); + } + + /** + * Test standard markdown features (these verify the upstream MarkdownExtra + * library works correctly through our transform() wrapper). + */ + function test_transform_bold(): void { + $md = new Markdown(); + $result = $md->transform( '**bold text**' ); + $this->assertStringContainsString( 'bold text', $result ); + } + + function test_transform_italic(): void { + $md = new Markdown(); + $result = $md->transform( '*italic text*' ); + $this->assertStringContainsString( 'italic text', $result ); + } + + function test_transform_link(): void { + $md = new Markdown(); + $result = $md->transform( '[Example](https://example.com)' ); + $this->assertStringContainsString( 'Example', $result ); + } + + function test_transform_unordered_list(): void { + $md = new Markdown(); + $result = $md->transform( "* Item 1\n* Item 2\n* Item 3" ); + $this->assertStringContainsString( '
      • Item 1
      • ', $result ); + $this->assertStringContainsString( '
          ', $result ); } } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php new file mode 100644 index 0000000000..f0192e1399 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php @@ -0,0 +1,253 @@ +assertTrue( post_type_exists( 'plugin' ) ); + } + + function test_plugin_post_type_is_public(): void { + $post_type = get_post_type_object( 'plugin' ); + $this->assertTrue( $post_type->public ); + } + + function test_plugin_post_type_has_rest_support(): void { + $post_type = get_post_type_object( 'plugin' ); + $this->assertTrue( $post_type->show_in_rest ); + } + + /** + * Test that expected taxonomies are registered. + * + * @dataProvider data_expected_taxonomies + */ + function test_taxonomy_registered( $taxonomy ): void { + $this->assertTrue( taxonomy_exists( $taxonomy ), "Taxonomy '{$taxonomy}' should be registered." ); + } + + function data_expected_taxonomies(): array { + return [ + 'plugin_section' => [ 'plugin_section' ], + 'plugin_tags' => [ 'plugin_tags' ], + 'plugin_category' => [ 'plugin_category' ], + 'plugin_contributors' => [ 'plugin_contributors' ], + 'plugin_built_for' => [ 'plugin_built_for' ], + 'plugin_business_model' => [ 'plugin_business_model' ], + 'plugin_committers' => [ 'plugin_committers' ], + 'plugin_support_reps' => [ 'plugin_support_reps' ], + ]; + } + + /** + * Test that custom post statuses are registered. + * + * @dataProvider data_expected_post_statuses + */ + function test_post_status_registered( $status ): void { + $this->assertNotFalse( + get_post_status_object( $status ), + "Post status '{$status}' should be registered." + ); + } + + function data_expected_post_statuses(): array { + return [ + 'new' => [ 'new' ], + 'pending' => [ 'pending' ], + 'disabled' => [ 'disabled' ], + 'approved' => [ 'approved' ], + 'closed' => [ 'closed' ], + 'rejected' => [ 'rejected' ], + ]; + } + + /** + * Test that a plugin post can be created. + */ + function test_can_create_plugin_post(): void { + $post_id = self::factory()->post->create( [ + 'post_type' => 'plugin', + 'post_title' => 'Test Plugin', + 'post_name' => 'test-plugin', + 'post_status' => 'publish', + ] ); + + $this->assertIsInt( $post_id ); + $this->assertGreaterThan( 0, $post_id ); + + $post = get_post( $post_id ); + $this->assertSame( 'plugin', $post->post_type ); + $this->assertSame( 'Test Plugin', $post->post_title ); + } + + /** + * Test that plugin meta fields can be stored and retrieved. + */ + function test_plugin_meta_fields(): void { + $post_id = self::factory()->post->create( [ + 'post_type' => 'plugin', + 'post_name' => 'meta-test', + 'post_status' => 'publish', + ] ); + + update_post_meta( $post_id, 'stable_tag', '2.0.0' ); + update_post_meta( $post_id, 'tested', '6.4' ); + update_post_meta( $post_id, 'requires', '5.0' ); + update_post_meta( $post_id, 'requires_php', '7.4' ); + update_post_meta( $post_id, 'active_installs', 50000 ); + update_post_meta( $post_id, 'downloads', 100000 ); + update_post_meta( $post_id, 'rating', 90 ); + + $this->assertSame( '2.0.0', get_post_meta( $post_id, 'stable_tag', true ) ); + $this->assertSame( '6.4', get_post_meta( $post_id, 'tested', true ) ); + $this->assertSame( '5.0', get_post_meta( $post_id, 'requires', true ) ); + $this->assertSame( '7.4', get_post_meta( $post_id, 'requires_php', true ) ); + $this->assertEquals( 50000, get_post_meta( $post_id, 'active_installs', true ) ); + $this->assertEquals( 100000, get_post_meta( $post_id, 'downloads', true ) ); + $this->assertEquals( 90, get_post_meta( $post_id, 'rating', true ) ); + } + + /** + * Test that terms can be assigned to the plugin_tags taxonomy. + */ + function test_assign_plugin_tags(): void { + $post_id = self::factory()->post->create( [ + 'post_type' => 'plugin', + 'post_status' => 'publish', + ] ); + + wp_set_object_terms( $post_id, [ 'seo', 'performance' ], 'plugin_tags' ); + + $terms = wp_get_object_terms( $post_id, 'plugin_tags', [ 'fields' => 'names' ] ); + $this->assertContains( 'seo', $terms ); + $this->assertContains( 'performance', $terms ); + } + + /** + * Test that terms can be assigned to the plugin_contributors taxonomy. + */ + function test_assign_plugin_contributors(): void { + $post_id = self::factory()->post->create( [ + 'post_type' => 'plugin', + 'post_status' => 'publish', + ] ); + + wp_set_object_terms( $post_id, [ 'johndoe' ], 'plugin_contributors' ); + + $terms = wp_get_object_terms( $post_id, 'plugin_contributors', [ 'fields' => 'names' ] ); + $this->assertContains( 'johndoe', $terms ); + } + + /** + * Test disabled post status is public (visible to non-logged-in users). + */ + function test_disabled_status_is_public(): void { + $status = get_post_status_object( 'disabled' ); + $this->assertTrue( $status->public ); + } + + /** + * Test closed post status is public (visible to non-logged-in users). + */ + function test_closed_status_is_public(): void { + $status = get_post_status_object( 'closed' ); + $this->assertTrue( $status->public ); + } + + /** + * Test that 'new' post status is not public. + */ + function test_new_status_is_not_public(): void { + $status = get_post_status_object( 'new' ); + $this->assertFalse( $status->public ); + } + + /** + * Test filter_wp_insert_post_data preserves post_modified for plugin posts. + */ + function test_filter_wp_insert_post_data_preserves_modified(): void { + $instance = Plugin_Directory::instance(); + + $data = [ + 'post_modified' => '2024-01-15 10:00:00', + 'post_modified_gmt' => '2024-01-15 10:00:00', + 'post_status' => 'publish', + 'post_name' => 'test', + ]; + $postarr = [ + 'post_type' => 'plugin', + 'post_modified' => '2024-01-15 10:00:00', + 'post_modified_gmt' => '2024-01-15 10:00:00', + 'post_status' => 'publish', + ]; + + $result = $instance->filter_wp_insert_post_data( $data, $postarr ); + + $this->assertSame( '2024-01-15 10:00:00', $result['post_modified'] ); + $this->assertSame( '2024-01-15 10:00:00', $result['post_modified_gmt'] ); + } + + /** + * Test filter_wp_insert_post_data ignores non-plugin post types. + */ + function test_filter_wp_insert_post_data_ignores_non_plugin(): void { + $instance = Plugin_Directory::instance(); + + $data = [ + 'post_modified' => '2024-01-15 10:00:00', + 'post_modified_gmt' => '2024-01-15 10:00:00', + 'post_status' => 'publish', + 'post_name' => 'test', + ]; + $postarr = [ + 'post_type' => 'post', + 'post_modified' => '2024-06-01 12:00:00', + 'post_modified_gmt' => '2024-06-01 12:00:00', + ]; + + $result = $instance->filter_wp_insert_post_data( $data, $postarr ); + + // Should return data unchanged for non-plugin types. + $this->assertSame( $data, $result ); + } + + /** + * Test filter_wp_insert_post_data preserves slug for pending plugin posts. + */ + function test_filter_wp_insert_post_data_preserves_pending_slug(): void { + $instance = Plugin_Directory::instance(); + + $post_id = self::factory()->post->create( [ + 'post_type' => 'plugin', + 'post_name' => 'my-pending-plugin', + 'post_status' => 'pending', + ] ); + + $data = [ + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', true ), + 'post_status' => 'pending', + 'post_name' => '', // WP clears slug for pending posts. + ]; + $postarr = [ + 'post_type' => 'plugin', + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', true ), + 'post_status' => 'pending', + 'ID' => $post_id, + ]; + + $result = $instance->filter_wp_insert_post_data( $data, $postarr ); + + $this->assertSame( 'my-pending-plugin', $result['post_name'] ); + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php new file mode 100644 index 0000000000..2ca38b1586 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php @@ -0,0 +1,360 @@ +validate( self::$valid_readme ); + + $this->assertEmpty( $result['errors'] ); + } + + function test_valid_readme_has_no_warnings(): void { + $validator = Validator::instance(); + $result = $validator->validate( self::$valid_readme ); + + $this->assertEmpty( $result['warnings'] ); + } + + function test_missing_plugin_name_is_error(): void { + $readme = <<<'README' +Contributors: johndoe +Stable tag: 1.0 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'invalid_plugin_name_header', $result['errors'] ); + } + + function test_missing_tested_is_warning(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Requires at least: 5.0 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'tested_header_missing', $result['warnings'] ); + } + + function test_missing_stable_tag_is_warning(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'stable_tag_invalid', $result['warnings'] ); + } + + function test_trunk_stable_tag_is_warning(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: trunk +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'stable_tag_invalid', $result['warnings'] ); + } + + function test_missing_requires_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'requires_header_missing', $result['notes'] ); + } + + function test_missing_requires_php_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +Requires at least: 5.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'requires_php_header_missing', $result['notes'] ); + } + + function test_missing_faq_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. + +== Changelog == + += 1.0 = +* Initial release. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'faq_missing', $result['notes'] ); + } + + function test_missing_changelog_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'changelog_missing', $result['notes'] ); + } + + function test_missing_screenshots_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'screenshots_missing', $result['notes'] ); + } + + function test_missing_donate_link_is_note(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 +License: GPLv2 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'donate_link_missing', $result['notes'] ); + } + + function test_missing_license_is_warning(): void { + $readme = <<<'README' +=== Test Plugin === +Contributors: johndoe +Tested up to: 6.4 +Stable tag: 1.0 + +Short description. + +== Description == + +Long description. +README; + $validator = Validator::instance(); + $result = $validator->validate( $readme ); + + $this->assertArrayHasKey( 'license_missing', $result['warnings'] ); + } + + /** + * Test translate_code_to_message returns strings for known codes. + * + * @dataProvider data_translate_code_to_message + */ + function test_translate_code_to_message( $code ): void { + $validator = Validator::instance(); + $result = $validator->translate_code_to_message( $code ); + + $this->assertIsString( $result ); + $this->assertNotEmpty( $result ); + } + + function data_translate_code_to_message(): array { + return [ + 'invalid name' => [ 'invalid_plugin_name_header' ], + 'tested missing' => [ 'tested_header_missing' ], + 'stable tag invalid' => [ 'stable_tag_invalid' ], + 'contributors missing' => [ 'contributors_missing' ], + 'faq missing' => [ 'faq_missing' ], + 'changelog missing' => [ 'changelog_missing' ], + 'screenshots missing' => [ 'screenshots_missing' ], + 'donate link missing' => [ 'donate_link_missing' ], + 'license missing' => [ 'license_missing' ], + 'requires missing' => [ 'requires_header_missing' ], + 'requires php missing' => [ 'requires_php_header_missing' ], + 'upgrade notice' => [ 'upgrade_notice_missing' ], + ]; + } + + function test_translate_code_to_message_unknown_code_returns_false(): void { + $validator = Validator::instance(); + $result = $validator->translate_code_to_message( 'nonexistent_code' ); + + $this->assertFalse( $result ); + } + + function test_translate_code_contributor_ignored_with_data(): void { + $validator = Validator::instance(); + $result = $validator->translate_code_to_message( 'contributor_ignored', [ 'fakeuser1', 'fakeuser2' ] ); + + $this->assertIsString( $result ); + $this->assertStringContainsString( 'fakeuser1', $result ); + $this->assertStringContainsString( 'fakeuser2', $result ); + } + + function test_translate_code_contributor_ignored_without_data(): void { + $validator = Validator::instance(); + $result = $validator->translate_code_to_message( 'contributor_ignored' ); + + $this->assertIsString( $result ); + $this->assertStringContainsString( 'Contributors', $result ); + } + + function test_validate_content_returns_translated_messages(): void { + $validator = Validator::instance(); + $result = $validator->validate_content( '' ); + + // validate_content translates error codes to human-readable strings. + foreach ( $result['errors'] as $message ) { + $this->assertIsString( $message ); + } + foreach ( $result['warnings'] as $message ) { + $this->assertIsString( $message ); + } + foreach ( $result['notes'] as $message ) { + $this->assertIsString( $message ); + } + } + + function test_last_content_stored(): void { + $validator = Validator::instance(); + $readme = "=== Test ===\nStable tag: 1.0\n\nDesc."; + $validator->validate( $readme ); + + $this->assertSame( $readme, $validator->last_content ); + } +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php index de2ce81aed..dff9399b28 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php @@ -100,9 +100,178 @@ function test_encode( $input, $expected ): void { function data_encode(): array { return [ - 'plain ascii' => [ 'Hello World', 'Hello World' ], - 'empty string' => [ '', '' ], - 'utf8 chars' => [ 'café', 'café' ], + 'plain ascii' => [ 'Hello World', 'Hello World' ], + 'empty string' => [ '', '' ], + 'numeric entity' => [ 'café', 'café' ], + 'utf8 e-acute' => [ "caf\xC3\xA9", 'café' ], + 'utf8 em-dash' => [ "\xE2\x80\x94", '—' ], + 'utf8 copyright' => [ "\xC2\xA9", '©' ], + 'windows-1252 e-acute' => [ "caf\xE9", 'café' ], + 'ampersand preserved' => [ 'A & B', 'A & B' ], + 'angle brackets' => [ 'a < b > c', 'a < b > c' ], ]; } + + /** + * Test dashicons_stars output. + * + * @dataProvider data_dashicons_stars + */ + function test_dashicons_stars( $rating, $filled, $half, $empty_stars ): void { + $output = Template::dashicons_stars( $rating ); + + $this->assertSame( $filled, substr_count( $output, 'dashicons-star-filled' ) ); + $this->assertSame( $half, substr_count( $output, 'dashicons-star-half' ) ); + $this->assertSame( $empty_stars, substr_count( $output, 'dashicons-star-empty' ) ); + } + + function data_dashicons_stars(): array { + // [ rating, filled, half, empty ] + return [ + 'zero stars' => [ 0, 0, 0, 5 ], + 'one star' => [ 1, 1, 0, 4 ], + 'two and a half' => [ 2.5, 2, 1, 2 ], + 'four stars' => [ 4, 4, 0, 1 ], + 'five stars' => [ 5, 5, 0, 0 ], + 'three point 3' => [ 3.3, 3, 1, 1 ], + ]; + } + + function test_get_close_reasons_returns_expected_keys(): void { + $reasons = Template::get_close_reasons(); + + $this->assertIsArray( $reasons ); + $this->assertArrayHasKey( 'security-issue', $reasons ); + $this->assertArrayHasKey( 'author-request', $reasons ); + $this->assertArrayHasKey( 'guideline-violation', $reasons ); + $this->assertArrayHasKey( 'licensing-trademark-violation', $reasons ); + $this->assertArrayHasKey( 'merged-into-core', $reasons ); + $this->assertArrayHasKey( 'unused', $reasons ); + } + + function test_get_rejection_reasons_returns_expected_keys(): void { + $reasons = Template::get_rejection_reasons(); + + $this->assertIsArray( $reasons ); + $this->assertArrayHasKey( '3-month', $reasons ); + $this->assertArrayHasKey( 'security', $reasons ); + $this->assertArrayHasKey( 'duplicate', $reasons ); + $this->assertArrayHasKey( 'banned', $reasons ); + } + + function test_download_link_with_version(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'test-plugin', + 'post_status' => 'publish', + ] ); + update_post_meta( $plugin->ID, 'stable_tag', '2.1.0' ); + + $link = Template::download_link( $plugin, '1.0.0' ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.1.0.0.zip', $link ); + + $link = Template::download_link( $plugin, 'latest' ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.2.1.0.zip', $link ); + + $link = Template::download_link( $plugin, 'trunk' ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.zip', $link ); + } + + function test_get_support_url_standard(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'my-plugin', + 'post_status' => 'publish', + ] ); + + $url = Template::get_support_url( $plugin ); + $this->assertSame( 'https://wordpress.org/support/plugin/my-plugin/', $url ); + } + + function test_get_support_url_buddypress(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'buddypress', + 'post_status' => 'publish', + ] ); + + $url = Template::get_support_url( $plugin ); + $this->assertSame( 'https://buddypress.org/support/', $url ); + } + + function test_get_support_url_bbpress(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'bbpress', + 'post_status' => 'publish', + ] ); + + $url = Template::get_support_url( $plugin ); + $this->assertSame( 'https://bbpress.org/forums/', $url ); + } + + function test_geopattern_icon_url_format(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'my-plugin', + 'post_status' => 'publish', + ] ); + + $url = Template::get_geopattern_icon_url( $plugin ); + $this->assertStringContainsString( 'my-plugin', $url ); + $this->assertStringContainsString( 'geopattern-icon', $url ); + $this->assertStringEndsWith( '.svg', $url ); + } + + function test_geopattern_icon_url_with_color(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'my-plugin', + 'post_status' => 'publish', + ] ); + + $url = Template::get_geopattern_icon_url( $plugin, 'ff5500' ); + $this->assertStringContainsString( 'my-plugin_ff5500', $url ); + } + + function test_geopattern_icon_url_with_invalid_color(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'my-plugin', + 'post_status' => 'publish', + ] ); + + // Invalid color should not appear in URL. + $url = Template::get_geopattern_icon_url( $plugin, 'notacolor' ); + $this->assertStringNotContainsString( '_notacolor', $url ); + } + + function test_is_plugin_outdated(): void { + $plugin = self::factory()->post->create_and_get( [ + 'post_type' => 'plugin', + 'post_name' => 'old-plugin', + 'post_status' => 'publish', + ] ); + + // Set tested to a very old version. + update_post_meta( $plugin->ID, 'tested', '4.0' ); + + $this->assertTrue( Template::is_plugin_outdated( $plugin ) ); + + // Set tested to current version. + $current = Template::get_current_major_wp_version(); + update_post_meta( $plugin->ID, 'tested', (string) $current ); + + $this->assertFalse( Template::is_plugin_outdated( $plugin ) ); + } + + function test_get_rollout_strategies(): void { + $strategies = Template::get_rollout_strategies(); + + $this->assertIsArray( $strategies ); + $this->assertArrayHasKey( '', $strategies ); + $this->assertArrayHasKey( 'manual-updates-24hr', $strategies ); + $this->assertArrayHasKey( 'name', $strategies[''] ); + $this->assertArrayHasKey( 'description', $strategies[''] ); + } } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php index 175877e2c6..79f97f7971 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/stubs/jetpack-search-stub.php @@ -26,5 +26,17 @@ public static function instance() { } public function setup() {} + + /** + * Returns the last query info (result counts, etc.). + * + * Referenced at class-plugin-search.php:437 — Plugin_Search uses this + * to retrieve Elasticsearch result totals for `$query->found_posts`. + * + * @return array|null + */ + public function get_last_query_info() { + return null; + } } } From 3bcadad17713198b8d8bc5b1425664b4d14e2525 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 13 Feb 2026 12:14:25 +1000 Subject: [PATCH 14/14] Fix test failures in Plugin Directory tests - Fix encode tests: ampersand and angle brackets are round-tripped by htmlentities + htmlspecialchars_decode (ENT_NOQUOTES), remove Windows-1252 test - Fix markdown code_trick tests: add surrounding text so trim() does not strip the 4-space indentation that code_trick adds for pre/code blocks - Fix readme validator: rename "My Test Plugin" to "My Test Tool" to avoid trademark check, create WP user for contributor validation - Fix all plugin post factory calls: add post_modified/post_modified_gmt fields to avoid undefined array key error in filter_wp_insert_post_data Co-Authored-By: Claude Opus 4.6 --- .../tests/phpunit/tests/test-markdown.php | 46 ++++----- .../phpunit/tests/test-plugin-directory.php | 60 ++++++------ .../phpunit/tests/test-readme-validator.php | 58 ++++++----- .../tests/phpunit/tests/test-template.php | 97 ++++++++----------- 4 files changed, 122 insertions(+), 139 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php index 1527c8e391..f7ded4b9d7 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-markdown.php @@ -51,25 +51,17 @@ function test_equals_header_with_leading_whitespace(): void { } /** - * Test code_trick:
           blocks are converted to markdown backtick format.
          +	 * Test code_trick: 
           blocks preserve underscores in code.
           	 *
           	 * This is custom logic in Markdown::code_trick() — pre-existing HTML code blocks
           	 * are converted to backtick format before markdown processing, so markdown
           	 * does not mangle underscores and other special characters inside code.
          +	 *
          +	 * The block needs surrounding content so trim() does not strip indentation.
           	 */
          -	function test_code_trick_pre_code_block(): void {
          -		$md     = new Markdown();
          -		$input  = "
          function my_func() { return true; }
          "; - $result = $md->transform( $input ); - - // The code should be preserved inside a code block. - $this->assertStringContainsString( 'my_func', $result ); - $this->assertStringContainsString( '', $result ); - } - function test_code_trick_preserves_underscores_in_pre_code(): void { - $md = new Markdown(); - $input = "
          \$my_var = some_function();
          "; + $md = new Markdown(); + $input = "Some text before.\n\n
          \$my_var = some_function();\n\$other_var = 1;
          \n\nSome text after."; $result = $md->transform( $input ); // Underscores should NOT be converted to tags inside code blocks. @@ -78,9 +70,9 @@ function test_code_trick_preserves_underscores_in_pre_code(): void { $this->assertStringContainsString( 'some_function', $result ); } - function test_code_trick_inline_code(): void { + function test_code_trick_inline_code_preserves_underscores(): void { $md = new Markdown(); - $input = "my_var_name"; + $input = "Use my_var_name for the setting."; $result = $md->transform( $input ); // Inline code should also preserve underscores. @@ -88,27 +80,25 @@ function test_code_trick_inline_code(): void { $this->assertStringContainsString( 'my_var_name', $result ); } - function test_code_trick_html_entities_decoded(): void { + /** + * Test code_trick: bbPress-style backtick code blocks at line start are + * converted to indented code (4 spaces) for markdown processing. + */ + function test_code_trick_bbpress_backtick_block(): void { $md = new Markdown(); - // HTML entities inside
           should be decoded back before markdown processing.
          -		$input  = "
          <div class="test">
          "; + $input = "Some text.\n\n`some_code_here`\n\nMore text."; $result = $md->transform( $input ); - $this->assertStringContainsString( '', $result ); + $this->assertStringContainsString( 'some_code_here', $result ); } /** - * Test code_trick: bbPress-style backtick code blocks are converted to - * indented code (4 spaces) for markdown processing. + * Test that inline markdown code (backticks) in mid-line is preserved. */ - function test_code_trick_bbpress_backtick_block(): void { + function test_inline_backtick_code_preserved(): void { $md = new Markdown(); - // bbPress-style block-level backtick code. - $input = "\n`some_code_here`"; - $result = $md->transform( $input ); - - // Should be rendered as a code block, not inline code. - $this->assertStringContainsString( 'some_code_here', $result ); + $result = $md->transform( 'Use `add_filter()` to modify output.' ); + $this->assertStringContainsString( 'add_filter()', $result ); } /** diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php index f0192e1399..2c44e870de 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-plugin-directory.php @@ -8,6 +8,22 @@ */ class Tests_Plugin_Directory extends WP_UnitTestCase { + /** + * Helper to create a plugin post with required fields to avoid + * "Undefined array key" errors in filter_wp_insert_post_data. + */ + private function create_plugin_post( $args = [] ) { + $now = current_time( 'mysql' ); + $gmt = current_time( 'mysql', true ); + + return self::factory()->post->create( array_merge( [ + 'post_type' => 'plugin', + 'post_status' => 'publish', + 'post_modified' => $now, + 'post_modified_gmt' => $gmt, + ], $args ) ); + } + /** * Test that the 'plugin' post type is registered. */ @@ -74,11 +90,9 @@ function data_expected_post_statuses(): array { * Test that a plugin post can be created. */ function test_can_create_plugin_post(): void { - $post_id = self::factory()->post->create( [ - 'post_type' => 'plugin', - 'post_title' => 'Test Plugin', - 'post_name' => 'test-plugin', - 'post_status' => 'publish', + $post_id = $this->create_plugin_post( [ + 'post_title' => 'Test Tool', + 'post_name' => 'test-tool', ] ); $this->assertIsInt( $post_id ); @@ -86,18 +100,14 @@ function test_can_create_plugin_post(): void { $post = get_post( $post_id ); $this->assertSame( 'plugin', $post->post_type ); - $this->assertSame( 'Test Plugin', $post->post_title ); + $this->assertSame( 'Test Tool', $post->post_title ); } /** * Test that plugin meta fields can be stored and retrieved. */ function test_plugin_meta_fields(): void { - $post_id = self::factory()->post->create( [ - 'post_type' => 'plugin', - 'post_name' => 'meta-test', - 'post_status' => 'publish', - ] ); + $post_id = $this->create_plugin_post( [ 'post_name' => 'meta-test' ] ); update_post_meta( $post_id, 'stable_tag', '2.0.0' ); update_post_meta( $post_id, 'tested', '6.4' ); @@ -120,10 +130,7 @@ function test_plugin_meta_fields(): void { * Test that terms can be assigned to the plugin_tags taxonomy. */ function test_assign_plugin_tags(): void { - $post_id = self::factory()->post->create( [ - 'post_type' => 'plugin', - 'post_status' => 'publish', - ] ); + $post_id = $this->create_plugin_post(); wp_set_object_terms( $post_id, [ 'seo', 'performance' ], 'plugin_tags' ); @@ -136,10 +143,7 @@ function test_assign_plugin_tags(): void { * Test that terms can be assigned to the plugin_contributors taxonomy. */ function test_assign_plugin_contributors(): void { - $post_id = self::factory()->post->create( [ - 'post_type' => 'plugin', - 'post_status' => 'publish', - ] ); + $post_id = $this->create_plugin_post(); wp_set_object_terms( $post_id, [ 'johndoe' ], 'plugin_contributors' ); @@ -226,28 +230,30 @@ function test_filter_wp_insert_post_data_ignores_non_plugin(): void { function test_filter_wp_insert_post_data_preserves_pending_slug(): void { $instance = Plugin_Directory::instance(); - $post_id = self::factory()->post->create( [ - 'post_type' => 'plugin', - 'post_name' => 'my-pending-plugin', + $now = current_time( 'mysql' ); + $gmt = current_time( 'mysql', true ); + + $post_id = $this->create_plugin_post( [ + 'post_name' => 'my-pending-tool', 'post_status' => 'pending', ] ); $data = [ - 'post_modified' => current_time( 'mysql' ), - 'post_modified_gmt' => current_time( 'mysql', true ), + 'post_modified' => $now, + 'post_modified_gmt' => $gmt, 'post_status' => 'pending', 'post_name' => '', // WP clears slug for pending posts. ]; $postarr = [ 'post_type' => 'plugin', - 'post_modified' => current_time( 'mysql' ), - 'post_modified_gmt' => current_time( 'mysql', true ), + 'post_modified' => $now, + 'post_modified_gmt' => $gmt, 'post_status' => 'pending', 'ID' => $post_id, ]; $result = $instance->filter_wp_insert_post_data( $data, $postarr ); - $this->assertSame( 'my-pending-plugin', $result['post_name'] ); + $this->assertSame( 'my-pending-tool', $result['post_name'] ); } } diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php index 2ca38b1586..09149494a4 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-readme-validator.php @@ -9,20 +9,21 @@ class Tests_Readme_Validator extends WP_UnitTestCase { protected static $valid_readme = <<<'README' -=== My Test Plugin === -Contributors: johndoe +=== My Test Tool === +Contributors: validatoruser Tags: test, unit-test Requires at least: 5.0 Tested up to: 6.4 Stable tag: 1.2.3 Requires PHP: 7.4 License: GPLv2 +Donate link: https://example.com/donate -A short description of the plugin. +A short description of the tool. == Description == -This is a longer description of the plugin. +This is a longer description of the tool. == Frequently Asked Questions == @@ -45,18 +46,23 @@ class Tests_Readme_Validator extends WP_UnitTestCase { Bug fix release. README; + static function wpSetUpBeforeClass( $factory ) { + // Create a user so the contributor sanitization finds them. + $factory->user->create( [ 'user_login' => 'validatoruser', 'user_nicename' => 'validatoruser' ] ); + } + function test_valid_readme_has_no_errors(): void { $validator = Validator::instance(); $result = $validator->validate( self::$valid_readme ); - $this->assertEmpty( $result['errors'] ); + $this->assertEmpty( $result['errors'], 'Expected no errors for valid readme. Got: ' . print_r( $result['errors'], true ) ); } function test_valid_readme_has_no_warnings(): void { $validator = Validator::instance(); $result = $validator->validate( self::$valid_readme ); - $this->assertEmpty( $result['warnings'] ); + $this->assertEmpty( $result['warnings'], 'Expected no warnings for valid readme. Got: ' . print_r( $result['warnings'], true ) ); } function test_missing_plugin_name_is_error(): void { @@ -78,8 +84,8 @@ function test_missing_plugin_name_is_error(): void { function test_missing_tested_is_warning(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Requires at least: 5.0 Stable tag: 1.0 License: GPLv2 @@ -98,8 +104,8 @@ function test_missing_tested_is_warning(): void { function test_missing_stable_tag_is_warning(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 License: GPLv2 @@ -117,8 +123,8 @@ function test_missing_stable_tag_is_warning(): void { function test_trunk_stable_tag_is_warning(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: trunk License: GPLv2 @@ -137,8 +143,8 @@ function test_trunk_stable_tag_is_warning(): void { function test_missing_requires_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 License: GPLv2 @@ -157,8 +163,8 @@ function test_missing_requires_is_note(): void { function test_missing_requires_php_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 Requires at least: 5.0 @@ -178,8 +184,8 @@ function test_missing_requires_php_is_note(): void { function test_missing_faq_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 License: GPLv2 @@ -203,8 +209,8 @@ function test_missing_faq_is_note(): void { function test_missing_changelog_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 License: GPLv2 @@ -223,8 +229,8 @@ function test_missing_changelog_is_note(): void { function test_missing_screenshots_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 License: GPLv2 @@ -243,8 +249,8 @@ function test_missing_screenshots_is_note(): void { function test_missing_donate_link_is_note(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 License: GPLv2 @@ -263,8 +269,8 @@ function test_missing_donate_link_is_note(): void { function test_missing_license_is_warning(): void { $readme = <<<'README' -=== Test Plugin === -Contributors: johndoe +=== Test Tool === +Contributors: validatoruser Tested up to: 6.4 Stable tag: 1.0 diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php index dff9399b28..8656dfeb80 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/phpunit/tests/test-template.php @@ -8,6 +8,22 @@ */ class Tests_Template extends WP_UnitTestCase { + /** + * Helper to create a plugin post with required fields to avoid + * "Undefined array key" errors in filter_wp_insert_post_data. + */ + private function create_plugin_post( $args = [] ) { + $now = current_time( 'mysql' ); + $gmt = current_time( 'mysql', true ); + + return self::factory()->post->create_and_get( array_merge( [ + 'post_type' => 'plugin', + 'post_status' => 'publish', + 'post_modified' => $now, + 'post_modified_gmt' => $gmt, + ], $args ) ); + } + /** * @dataProvider data_sanitize_active_installs */ @@ -100,15 +116,15 @@ function test_encode( $input, $expected ): void { function data_encode(): array { return [ - 'plain ascii' => [ 'Hello World', 'Hello World' ], - 'empty string' => [ '', '' ], - 'numeric entity' => [ 'café', 'café' ], - 'utf8 e-acute' => [ "caf\xC3\xA9", 'café' ], - 'utf8 em-dash' => [ "\xE2\x80\x94", '—' ], - 'utf8 copyright' => [ "\xC2\xA9", '©' ], - 'windows-1252 e-acute' => [ "caf\xE9", 'café' ], - 'ampersand preserved' => [ 'A & B', 'A & B' ], - 'angle brackets' => [ 'a < b > c', 'a < b > c' ], + 'plain ascii' => [ 'Hello World', 'Hello World' ], + 'empty string' => [ '', '' ], + 'numeric entity' => [ 'café', 'café' ], + 'utf8 e-acute' => [ "caf\xC3\xA9", 'café' ], + 'utf8 em-dash' => [ "\xE2\x80\x94", '—' ], + 'utf8 copyright' => [ "\xC2\xA9", '©' ], + // encode() round-trips & < > via htmlentities + htmlspecialchars_decode (ENT_NOQUOTES). + 'ampersand' => [ 'A & B', 'A & B' ], + 'angle brackets' => [ 'a < b > c', 'a < b > c' ], ]; } @@ -160,108 +176,73 @@ function test_get_rejection_reasons_returns_expected_keys(): void { } function test_download_link_with_version(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'test-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'test-download' ] ); update_post_meta( $plugin->ID, 'stable_tag', '2.1.0' ); $link = Template::download_link( $plugin, '1.0.0' ); - $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.1.0.0.zip', $link ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.1.0.0.zip', $link ); $link = Template::download_link( $plugin, 'latest' ); - $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.2.1.0.zip', $link ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.2.1.0.zip', $link ); $link = Template::download_link( $plugin, 'trunk' ); - $this->assertSame( 'https://downloads.wordpress.org/plugin/test-plugin.zip', $link ); + $this->assertSame( 'https://downloads.wordpress.org/plugin/test-download.zip', $link ); } function test_get_support_url_standard(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'my-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'my-cool-tool' ] ); $url = Template::get_support_url( $plugin ); - $this->assertSame( 'https://wordpress.org/support/plugin/my-plugin/', $url ); + $this->assertSame( 'https://wordpress.org/support/plugin/my-cool-tool/', $url ); } function test_get_support_url_buddypress(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'buddypress', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'buddypress' ] ); $url = Template::get_support_url( $plugin ); $this->assertSame( 'https://buddypress.org/support/', $url ); } function test_get_support_url_bbpress(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'bbpress', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'bbpress' ] ); $url = Template::get_support_url( $plugin ); $this->assertSame( 'https://bbpress.org/forums/', $url ); } function test_geopattern_icon_url_format(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'my-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-test' ] ); $url = Template::get_geopattern_icon_url( $plugin ); - $this->assertStringContainsString( 'my-plugin', $url ); + $this->assertStringContainsString( 'geo-test', $url ); $this->assertStringContainsString( 'geopattern-icon', $url ); $this->assertStringEndsWith( '.svg', $url ); } function test_geopattern_icon_url_with_color(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'my-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-color' ] ); $url = Template::get_geopattern_icon_url( $plugin, 'ff5500' ); - $this->assertStringContainsString( 'my-plugin_ff5500', $url ); + $this->assertStringContainsString( 'geo-color_ff5500', $url ); } function test_geopattern_icon_url_with_invalid_color(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'my-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'geo-invalid' ] ); - // Invalid color should not appear in URL. $url = Template::get_geopattern_icon_url( $plugin, 'notacolor' ); $this->assertStringNotContainsString( '_notacolor', $url ); } function test_is_plugin_outdated(): void { - $plugin = self::factory()->post->create_and_get( [ - 'post_type' => 'plugin', - 'post_name' => 'old-plugin', - 'post_status' => 'publish', - ] ); + $plugin = $this->create_plugin_post( [ 'post_name' => 'old-tool' ] ); // Set tested to a very old version. update_post_meta( $plugin->ID, 'tested', '4.0' ); - $this->assertTrue( Template::is_plugin_outdated( $plugin ) ); // Set tested to current version. $current = Template::get_current_major_wp_version(); update_post_meta( $plugin->ID, 'tested', (string) $current ); - $this->assertFalse( Template::is_plugin_outdated( $plugin ) ); }