From 699949c35ae05dc82b87efc831b8c7b1c4f8b3f5 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 09:18:55 +0600 Subject: [PATCH 1/2] Tests: Add unit test coverage for the row meta in WP_Plugins_List_Table::single_row(). --- .../tests/admin/wpPluginsListTableRowMeta.php | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php diff --git a/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php b/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php new file mode 100644 index 0000000000000..5bfc9d53dbd42 --- /dev/null +++ b/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php @@ -0,0 +1,315 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$activator_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + get_userdata( self::$activator_id )->add_cap( 'activate_plugins' ); + + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + } + + /** + * Revokes the super admin grant before the fixture users are deleted. + */ + public static function wpTearDownAfterClass() { + if ( is_multisite() ) { + revoke_super_admin( self::$admin_id ); + } + } + + /** + * Sets up the Plugins screen and the list table globals. + */ + public function set_up() { + parent::set_up(); + + foreach ( array( 'status', 'page', 's', 'totals', 'current_screen' ) as $name ) { + $this->original_globals[ $name ] = $GLOBALS[ $name ] ?? null; + } + + set_current_screen( 'plugins' ); + + $GLOBALS['status'] = 'all'; + $GLOBALS['page'] = 1; + $GLOBALS['s'] = ''; + $GLOBALS['totals'] = array(); + + $this->filter_args = null; + $this->slug = 'row-meta-plugin-' . ++self::$counter; + + add_filter( 'plugin_row_meta', array( $this, 'capture_meta' ), PHP_INT_MAX, 4 ); + + wp_set_current_user( self::$admin_id ); + } + + /** + * Restores the globals and removes the test plugin. + */ + public function tear_down() { + foreach ( $this->original_globals as $name => $value ) { + $GLOBALS[ $name ] = $value; + } + + $dir = WP_PLUGIN_DIR . '/' . $this->slug; + if ( is_dir( $dir ) ) { + unlink( $dir . '/' . $this->slug . '.php' ); + rmdir( $dir ); + } + + parent::tear_down(); + } + + /** + * Tests the version, linked author and View details link of a plugin from the directory. + * + * @ticket 65819 + */ + public function test_should_list_version_author_and_details_link() { + $this->assertSame( + array( + 'Version 1.2.3', + 'By Jane', + 'View details', + ), + $this->get_meta( + array( + 'Name' => 'My "Plugin"', + 'Version' => '1.2.3', + 'Author' => 'Jane', + 'AuthorURI' => 'https://example.org/jane/', + 'slug' => $this->slug, + ) + ) + ); + } + + /** + * Tests the author without a link and the plugin site link of a plugin that is not from the directory. + * + * @ticket 65819 + */ + public function test_should_link_plugin_site_without_directory_slug() { + $this->assertSame( + array( + 'Version 1.2.3', + 'By Jane', + 'Visit plugin site', + ), + $this->get_meta( + array( + 'Version' => '1.2.3', + 'Author' => 'Jane', + 'PluginURI' => 'https://example.org/plugin/?a=1&b=2', + ) + ) + ); + } + + /** + * Tests that a user who cannot install plugins gets the plugin site link instead of View details. + * + * @ticket 65819 + */ + public function test_should_link_plugin_site_for_user_without_install_capability() { + wp_set_current_user( self::$activator_id ); + + $this->assertSame( + array( 'Visit plugin site' ), + $this->get_meta( + array( + 'slug' => $this->slug, + 'PluginURI' => 'https://example.org/plugin/', + ) + ) + ); + } + + /** + * Tests that empty fields are left out. + * + * @ticket 65819 + * + * @dataProvider data_empty_values + * + * @param mixed $value The empty value of the version, author, author URI and plugin URI. + */ + public function test_should_leave_out_empty_fields( $value ) { + $this->assertSame( + array(), + $this->get_meta( + array( + 'Version' => $value, + 'Author' => $value, + 'AuthorURI' => 'https://example.org/jane/', + 'PluginURI' => $value, + ) + ) + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_empty_values() { + return array( + 'empty string' => array( '' ), + 'zero string' => array( '0' ), + ); + } + + /** + * Tests that the filter receives the plugin file, data and status, and its links are joined. + * + * @ticket 65819 + */ + public function test_should_filter_and_join_meta() { + add_filter( + 'plugin_row_meta', + static function ( $meta ) { + $meta[] = 'Docs'; + + return $meta; + } + ); + + $output = $this->render( array( 'Version' => '1.2.3' ) ); + + $this->assertSame( $this->get_file(), $this->filter_args[1], 'The filter should receive the plugin file.' ); + $this->assertSame( 'My Plugin', $this->filter_args[2]['Name'], 'The filter should receive the plugin data.' ); + $this->assertSame( 'all', $this->filter_args[3], 'The filter should receive the status.' ); + $this->assertStringContainsString( 'plugin-version-author-uri\'>Version 1.2.3 | Docs', $output, 'The links should be joined.' ); + } + + /** + * Captures the arguments of the plugin_row_meta filter. + * + * @param string[] $meta The row meta. + * @param string $plugin_file The plugin file. + * @param array $plugin_data The plugin data. + * @param string $status The plugin status. + * @return string[] The unchanged row meta. + */ + public function capture_meta( $meta, $plugin_file, $plugin_data, $status ) { + $this->filter_args = array( $meta, $plugin_file, $plugin_data, $status ); + + return $meta; + } + + /** + * Renders the row of the test plugin and returns its row meta. + * + * @param array $plugin_data The plugin data to add. + * @return string[] The row meta, before the plugin_row_meta filter. + */ + private function get_meta( $plugin_data ) { + $this->render( $plugin_data ); + + return $this->filter_args[0]; + } + + /** + * Renders the row of the test plugin. + * + * @param array $plugin_data The plugin data to add. + * @return string The row markup. + */ + private function render( $plugin_data ) { + // is_network_only_plugin() reads the plugin headers on multisite. + $dir = WP_PLUGIN_DIR . '/' . $this->slug; + $this->assertTrue( mkdir( $dir ), 'The test plugin directory should be created.' ); + $this->assertNotFalse( file_put_contents( $dir . '/' . $this->slug . '.php', " 'plugins' ) ); + + return get_echo( + array( $table, 'single_row' ), + array( + array( + $this->get_file(), + array_merge( + array( + 'Name' => 'My Plugin', + 'Description' => '', + 'Version' => '', + 'Author' => '', + 'AuthorURI' => '', + 'PluginURI' => '', + ), + $plugin_data + ), + ), + ) + ); + } + + /** + * Returns the plugin file of the test plugin. + * + * @return string The plugin file. + */ + private function get_file() { + return "{$this->slug}/{$this->slug}.php"; + } +} From 4f70c2e63fa99a41fca99abe162187fe54629025 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 11:44:49 +0600 Subject: [PATCH 2/2] Tests: Remove a leftover test plugin before creating it in the WP_Plugins_List_Table row meta tests. --- .../tests/admin/wpPluginsListTableRowMeta.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php b/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php index 5bfc9d53dbd42..9f268e470cfb7 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php @@ -109,11 +109,7 @@ public function tear_down() { $GLOBALS[ $name ] = $value; } - $dir = WP_PLUGIN_DIR . '/' . $this->slug; - if ( is_dir( $dir ) ) { - unlink( $dir . '/' . $this->slug . '.php' ); - rmdir( $dir ); - } + $this->remove_plugin(); parent::tear_down(); } @@ -278,6 +274,7 @@ private function get_meta( $plugin_data ) { private function render( $plugin_data ) { // is_network_only_plugin() reads the plugin headers on multisite. $dir = WP_PLUGIN_DIR . '/' . $this->slug; + $this->remove_plugin(); $this->assertTrue( mkdir( $dir ), 'The test plugin directory should be created.' ); $this->assertNotFalse( file_put_contents( $dir . '/' . $this->slug . '.php', "slug}/{$this->slug}.php"; } + + /** + * Removes the test plugin, including one left over by an interrupted run. + */ + private function remove_plugin() { + $dir = WP_PLUGIN_DIR . '/' . $this->slug; + + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + rmdir( $dir ); + } + } }