From 07bf9228cfe624f0879aaa6b11f1ccc89ab063fb Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 05:40:53 +0600 Subject: [PATCH 1/6] Tests: Add unit test coverage for the site screen action links in WP_Plugins_List_Table::single_row(). --- .../admin/wpPluginsListTableActionLinks.php | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php new file mode 100644 index 0000000000000..b7fa18c1ab976 --- /dev/null +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -0,0 +1,458 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + + 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, the list table globals and the dependency state. + */ + public function set_up() { + parent::set_up(); + + foreach ( array( 'status', 'page', 's', 'totals', '_paused_plugins', '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(); + $GLOBALS['_paused_plugins'] = array(); + + $this->set_property_value( 'plugins', array() ); + $this->set_property_value( 'dependencies', array() ); + $this->set_property_value( 'dependency_slugs', array() ); + $this->set_property_value( 'dependency_filepaths', array() ); + $this->set_property_value( 'circular_dependencies_slugs', array() ); + $this->set_property_value( 'circular_dependencies_pairs', array() ); + $this->set_property_value( 'initialized', true ); + + update_option( 'active_plugins', array() ); + update_site_option( 'active_sitewide_plugins', array() ); + + $this->actions = null; + $this->slug = 'action-links-plugin-' . ++self::$counter; + + add_filter( 'plugin_action_links', array( $this, 'capture_actions' ), PHP_INT_MAX ); + + 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 Deactivate link of an active plugin. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_link_deactivate_for_active_plugin() { + $this->activate(); + + $this->assertSame( + array( + 'deactivate' => 'Deactivate', + ), + $this->get_actions() + ); + } + + /** + * Tests that an active plugin that other active plugins require cannot be deactivated. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_not_link_deactivate_for_plugin_with_active_dependents() { + $this->set_dependent( true ); + $this->activate(); + + $this->assertSame( + array( + 'deactivate' => 'DeactivateYou cannot deactivate this plugin as other plugins depend on it.', + ), + $this->get_actions() + ); + } + + /** + * Tests the Resume link of an active plugin that was paused. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_link_resume_for_paused_plugin() { + $this->activate(); + $GLOBALS['_paused_plugins'] = array( + $this->slug => array( + 'type' => E_ERROR, + 'line' => 1, + 'file' => WP_PLUGIN_DIR . '/' . $this->get_file(), + 'message' => 'Error.', + ), + ); + + $this->assertSame( + 'Resume', + $this->get_actions()['resume'] + ); + } + + /** + * Tests the Activate and Delete links of an inactive plugin. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_link_activate_and_delete_for_inactive_plugin() { + $this->assertSame( + array( + 'activate' => 'Activate', + 'delete' => 'Delete', + ), + $this->get_actions() + ); + } + + /** + * Tests that a plugin whose requirements are not met cannot be activated. + * + * @ticket 65819 + * + * @group ms-excluded + * + * @dataProvider data_unmet_requirements + * + * @param array $plugin_data The plugin's requirement headers. + */ + public function test_should_not_activate_plugin_with_unmet_requirements( $plugin_data ) { + $this->assertSame( 'Cannot Activate', $this->get_actions( $plugin_data )['activate'] ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unmet_requirements() { + return array( + 'PHP version' => array( array( 'RequiresPHP' => '99.0' ) ), + 'WordPress version' => array( array( 'RequiresWP' => '99.0' ) ), + ); + } + + /** + * Tests that a plugin with unmet dependencies cannot be activated. + * + * @ticket 65819 + * + * @group ms-excluded + */ + public function test_should_not_activate_plugin_with_unmet_dependencies() { + $this->set_property_value( 'dependencies', array( $this->get_file() => array( 'missing' ) ) ); + $this->set_property_value( 'dependency_filepaths', array( 'missing' => false ) ); + + $this->assertSame( + 'ActivateYou cannot activate this plugin as it has unmet requirements.', + $this->get_actions()['activate'] + ); + } + + /** + * Tests that a plugin that other plugins require cannot be deleted, unless the dependency is circular. + * + * @ticket 65819 + * + * @group ms-excluded + * + * @dataProvider data_circular_dependencies + * + * @param bool $circular Whether the dependency is circular. + */ + public function test_delete_link_for_plugin_with_dependents( $circular ) { + $this->set_dependent( false ); + + if ( $circular ) { + $this->set_property_value( 'circular_dependencies_slugs', array( $this->slug ) ); + } + + $this->assertSame( + $circular + ? 'Delete' + : 'DeleteYou cannot delete this plugin as other plugins require it.', + $this->get_actions()['delete'] + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_circular_dependencies() { + return array( + 'regular dependency' => array( false ), + 'circular dependency' => array( true ), + ); + } + + /** + * Tests that a user who cannot manage plugins gets no action links. + * + * @ticket 65819 + * + * @dataProvider data_active_states + * + * @param bool $active Whether the plugin is active. + */ + public function test_should_not_add_links_for_user_without_capabilities( $active ) { + wp_set_current_user( self::$subscriber_id ); + + if ( $active ) { + $this->activate(); + } + + $this->assertSame( array(), $this->get_actions() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_active_states() { + return array( + 'active plugin' => array( true ), + 'inactive plugin' => array( false ), + ); + } + + /** + * Tests that a network-active plugin only shows that it is network active on a site. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_only_show_network_active_on_site_screen() { + update_site_option( 'active_sitewide_plugins', array( $this->get_file() => time() ) ); + + $this->assertSame( array( 'network_active' => 'Network Active' ), $this->get_actions() ); + } + + /** + * Tests that the filter receives the plugin file, data and context. + * + * @ticket 65819 + */ + public function test_should_pass_plugin_and_context_to_filter() { + $filter = new MockAction(); + add_filter( 'plugin_action_links', array( $filter, 'filter' ), 10, 4 ); + + $this->get_actions(); + + $args = $filter->get_args(); + + $this->assertSame( $this->get_file(), $args[0][1], 'The filter should receive the plugin file.' ); + $this->assertSame( 'My Plugin', $args[0][2]['Name'], 'The filter should receive the plugin data.' ); + $this->assertSame( 'all', $args[0][3], 'The filter should receive the context.' ); + } + + /** + * Captures the actions passed to the plugin_action_links filter. + * + * @param array $actions The action links. + * @return array The unchanged action links. + */ + public function capture_actions( $actions ) { + $this->actions = $actions; + + return $actions; + } + + /** + * Renders the row of the test plugin and returns its action links. + * + * @param array $plugin_data The plugin data to add. + * @return array The action links. + */ + private function get_actions( $plugin_data = array() ) { + // 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' ) ); + + get_echo( + array( $table, 'single_row' ), + array( + array( + $this->get_file(), + array_merge( + array( + 'Name' => 'My Plugin', + 'slug' => $this->slug, + 'Description' => '', + 'Version' => '1.0', + 'Author' => '', + 'AuthorURI' => '', + 'PluginURI' => '', + ), + $plugin_data + ), + ), + ) + ); + + return $this->actions; + } + + /** + * Returns the plugin file of the test plugin. + * + * @return string The plugin file. + */ + private function get_file() { + return "{$this->slug}/{$this->slug}.php"; + } + + /** + * Returns the expected URL of an action. + * + * @param string $action The action. + * @param string $nonce The nonce action. + * @return string The escaped URL. + */ + private function get_url( $action, $nonce ) { + return "plugins.php?action={$action}&plugin={$this->slug}%2F{$this->slug}.php&plugin_status=all&paged=1&s&_wpnonce=" . wp_create_nonce( $nonce ); + } + + /** + * Returns the expected Delete URL. + * + * @return string The escaped URL. + */ + private function get_delete_url() { + return "plugins.php?action=delete-selected&checked%5B0%5D={$this->slug}%2F{$this->slug}.php&plugin_status=all&paged=1&s&_wpnonce=" . wp_create_nonce( 'bulk-plugins' ); + } + + /** + * Activates the test plugin. + */ + private function activate() { + update_option( 'active_plugins', array_merge( get_option( 'active_plugins' ), array( $this->get_file() ) ) ); + } + + /** + * Makes another plugin require the test plugin. + * + * @param bool $active Whether the other plugin is active. + */ + private function set_dependent( $active ) { + $this->set_property_value( 'plugins', array( 'dependent/dependent.php' => array( 'Name' => 'Dependent' ) ) ); + $this->set_property_value( 'dependencies', array( 'dependent/dependent.php' => array( $this->slug ) ) ); + $this->set_property_value( 'dependency_slugs', array( $this->slug ) ); + + if ( $active ) { + update_option( 'active_plugins', array( 'dependent/dependent.php' ) ); + } + } +} From 46052039e9f234d7067da9972a7cba24967e200c Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 21 Oct 2026 11:24:40 +0600 Subject: [PATCH 2/6] Tests: Remove a leftover test plugin before creating it in the WP_Plugins_List_Table action links tests. --- .../admin/wpPluginsListTableActionLinks.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php index b7fa18c1ab976..2cc3211a51091 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -124,11 +124,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(); } @@ -376,6 +372,7 @@ public function capture_actions( $actions ) { private function get_actions( $plugin_data = array() ) { // 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; + + if ( is_dir( $dir ) ) { + $this->rmdir( $dir ); + rmdir( $dir ); + } + } } From c1c44ece68b31ef9c2fe06fa3e208af7b3890a20 Mon Sep 17 00:00:00 2001 From: Lance Willett Date: Tue, 27 Oct 2026 19:11:05 -0700 Subject: [PATCH 3/6] Tests: Check inactive plugin actions on multisite site screens. --- .../admin/wpPluginsListTableActionLinks.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php index 2cc3211a51091..1e3da3cfa0aa4 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -194,17 +194,17 @@ public function test_should_link_resume_for_paused_plugin() { * Tests the Activate and Delete links of an inactive plugin. * * @ticket 65819 - * - * @group ms-excluded */ public function test_should_link_activate_and_delete_for_inactive_plugin() { - $this->assertSame( - array( - 'activate' => 'Activate', - 'delete' => 'Delete', - ), - $this->get_actions() + $expected_actions = array( + 'activate' => 'Activate', ); + + if ( ! is_multisite() ) { + $expected_actions['delete'] = 'Delete'; + } + + $this->assertSame( $expected_actions, $this->get_actions() ); } /** From 67e85b8995a8484a1e26b05118cca1a98adc70ec Mon Sep 17 00:00:00 2001 From: Lance Willett Date: Tue, 27 Oct 2026 19:35:54 -0700 Subject: [PATCH 4/6] Tests: Check Resume permission in the existing subscriber fixture. --- .../phpunit/tests/admin/wpPluginsListTableActionLinks.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php index 1e3da3cfa0aa4..113536a5aacd5 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -303,6 +303,14 @@ public function test_should_not_add_links_for_user_without_capabilities( $active if ( $active ) { $this->activate(); + $GLOBALS['_paused_plugins'] = array( + $this->slug => array( + 'type' => E_ERROR, + 'line' => 1, + 'file' => WP_PLUGIN_DIR . '/' . $this->get_file(), + 'message' => 'Error.', + ), + ); } $this->assertSame( array(), $this->get_actions() ); From 79b5d7af33454661ca0ea05a8dbb0c281bf6c2bb Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Wed, 28 Oct 2026 19:55:14 +0600 Subject: [PATCH 5/6] Tests: Improve multisite coverage and fixture isolation for Plugins List Table site action links. --- .../admin/wpPluginsListTableActionLinks.php | 152 ++++++++++++------ 1 file changed, 102 insertions(+), 50 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php index 113536a5aacd5..859c92778ff5f 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -27,20 +27,18 @@ class Tests_Admin_wpPluginsListTableActionLinks extends WP_PluginDependencies_Un private static $subscriber_id; /** - * Number used to give each rendered plugin a unique slug. - * - * single_row() keeps the ID attributes it used in a static variable. + * The original globals the list table reads. * - * @var int + * @var array */ - private static $counter = 0; + private $original_globals = array(); /** - * The original globals the list table reads. + * The original request parameters. * * @var array */ - private $original_globals = array(); + private $original_request = array(); /** * The actions passed to the plugin_action_links filter. @@ -85,14 +83,23 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); - foreach ( array( 'status', 'page', 's', 'totals', '_paused_plugins', 'current_screen' ) as $name ) { - $this->original_globals[ $name ] = $GLOBALS[ $name ] ?? null; + $this->original_globals = array(); + foreach ( array( 's', 'totals', '_paused_plugins' ) as $name ) { + if ( array_key_exists( $name, $GLOBALS ) ) { + $this->original_globals[ $name ] = $GLOBALS[ $name ]; + } } + $this->original_request = array( + 'plugin_status' => $_REQUEST['plugin_status'] ?? null, + 'paged' => $_REQUEST['paged'] ?? null, + ); + set_current_screen( 'plugins' ); - $GLOBALS['status'] = 'all'; - $GLOBALS['page'] = 1; + $_REQUEST['plugin_status'] = 'all'; + $_REQUEST['paged'] = 1; + $GLOBALS['s'] = ''; $GLOBALS['totals'] = array(); $GLOBALS['_paused_plugins'] = array(); @@ -109,7 +116,7 @@ public function set_up() { update_site_option( 'active_sitewide_plugins', array() ); $this->actions = null; - $this->slug = 'action-links-plugin-' . ++self::$counter; + $this->slug = 'action-links-plugin-' . wp_generate_password( 8, false ); add_filter( 'plugin_action_links', array( $this, 'capture_actions' ), PHP_INT_MAX ); @@ -124,6 +131,20 @@ public function tear_down() { $GLOBALS[ $name ] = $value; } + foreach ( array( 's', 'totals', '_paused_plugins' ) as $name ) { + if ( ! array_key_exists( $name, $this->original_globals ) ) { + unset( $GLOBALS[ $name ] ); + } + } + + foreach ( $this->original_request as $key => $value ) { + if ( null !== $value ) { + $_REQUEST[ $key ] = $value; + } else { + unset( $_REQUEST[ $key ] ); + } + } + $this->remove_plugin(); parent::tear_down(); @@ -133,8 +154,6 @@ public function tear_down() { * Tests the Deactivate link of an active plugin. * * @ticket 65819 - * - * @group ms-excluded */ public function test_should_link_deactivate_for_active_plugin() { $this->activate(); @@ -151,8 +170,6 @@ public function test_should_link_deactivate_for_active_plugin() { * Tests that an active plugin that other active plugins require cannot be deactivated. * * @ticket 65819 - * - * @group ms-excluded */ public function test_should_not_link_deactivate_for_plugin_with_active_dependents() { $this->set_dependent( true ); @@ -170,23 +187,17 @@ public function test_should_not_link_deactivate_for_plugin_with_active_dependent * Tests the Resume link of an active plugin that was paused. * * @ticket 65819 - * - * @group ms-excluded */ public function test_should_link_resume_for_paused_plugin() { $this->activate(); - $GLOBALS['_paused_plugins'] = array( - $this->slug => array( - 'type' => E_ERROR, - 'line' => 1, - 'file' => WP_PLUGIN_DIR . '/' . $this->get_file(), - 'message' => 'Error.', - ), - ); + $this->pause(); $this->assertSame( - 'Resume', - $this->get_actions()['resume'] + array( + 'deactivate' => 'Deactivate', + 'resume' => 'Resume', + ), + $this->get_actions() ); } @@ -201,7 +212,7 @@ public function test_should_link_activate_and_delete_for_inactive_plugin() { ); if ( ! is_multisite() ) { - $expected_actions['delete'] = 'Delete'; + $expected_actions['delete'] = $this->get_delete_link(); } $this->assertSame( $expected_actions, $this->get_actions() ); @@ -212,8 +223,6 @@ public function test_should_link_activate_and_delete_for_inactive_plugin() { * * @ticket 65819 * - * @group ms-excluded - * * @dataProvider data_unmet_requirements * * @param array $plugin_data The plugin's requirement headers. @@ -238,8 +247,6 @@ public static function data_unmet_requirements() { * Tests that a plugin with unmet dependencies cannot be activated. * * @ticket 65819 - * - * @group ms-excluded */ public function test_should_not_activate_plugin_with_unmet_dependencies() { $this->set_property_value( 'dependencies', array( $this->get_file() => array( 'missing' ) ) ); @@ -271,7 +278,7 @@ public function test_delete_link_for_plugin_with_dependents( $circular ) { $this->assertSame( $circular - ? 'Delete' + ? $this->get_delete_link() : 'DeleteYou cannot delete this plugin as other plugins require it.', $this->get_actions()['delete'] ); @@ -303,14 +310,7 @@ public function test_should_not_add_links_for_user_without_capabilities( $active if ( $active ) { $this->activate(); - $GLOBALS['_paused_plugins'] = array( - $this->slug => array( - 'type' => E_ERROR, - 'line' => 1, - 'file' => WP_PLUGIN_DIR . '/' . $this->get_file(), - 'message' => 'Error.', - ), - ); + $this->pause(); } $this->assertSame( array(), $this->get_actions() ); @@ -341,6 +341,20 @@ public function test_should_only_show_network_active_on_site_screen() { $this->assertSame( array( 'network_active' => 'Network Active' ), $this->get_actions() ); } + /** + * Tests that an inactive network-only plugin shows "Network Only" on a site's Plugins screen. + * + * @ticket 65819 + * + * @group ms-required + */ + public function test_should_show_network_only_for_inactive_network_only_plugin() { + $this->assertSame( + array( 'network_only' => 'Network Only' ), + $this->get_actions( array(), " * Network: true\n" ) + ); + } + /** * Tests that the filter receives the plugin file, data and context. * @@ -374,15 +388,30 @@ public function capture_actions( $actions ) { /** * Renders the row of the test plugin and returns its action links. * - * @param array $plugin_data The plugin data to add. + * @param array $plugin_data The plugin data to add. + * @param string $headers Additional plugin headers for the file on multisite. * @return array The action links. */ - private function get_actions( $plugin_data = array() ) { + private function get_actions( $plugin_data = array(), $headers = '' ) { // 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', "markTestSkipped( 'WP_PLUGIN_DIR is not writable.' ); + } + + $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', + " 'plugins' ) ); @@ -439,6 +468,15 @@ private function get_delete_url() { return "plugins.php?action=delete-selected&checked%5B0%5D={$this->slug}%2F{$this->slug}.php&plugin_status=all&paged=1&s&_wpnonce=" . wp_create_nonce( 'bulk-plugins' ); } + /** + * Returns the expected Delete link markup. + * + * @return string The Delete link markup. + */ + private function get_delete_link() { + return 'Delete'; + } + /** * Activates the test plugin. */ @@ -446,6 +484,20 @@ private function activate() { update_option( 'active_plugins', array_merge( get_option( 'active_plugins' ), array( $this->get_file() ) ) ); } + /** + * Pauses the test plugin. + */ + private function pause() { + $GLOBALS['_paused_plugins'] = array( + $this->slug => array( + 'type' => E_ERROR, + 'line' => 1, + 'file' => WP_PLUGIN_DIR . '/' . $this->get_file(), + 'message' => 'Error.', + ), + ); + } + /** * Makes another plugin require the test plugin. * @@ -457,7 +509,7 @@ private function set_dependent( $active ) { $this->set_property_value( 'dependency_slugs', array( $this->slug ) ); if ( $active ) { - update_option( 'active_plugins', array( 'dependent/dependent.php' ) ); + update_option( 'active_plugins', array_merge( (array) get_option( 'active_plugins', array() ), array( 'dependent/dependent.php' ) ) ); } } @@ -469,7 +521,7 @@ private function remove_plugin() { if ( is_dir( $dir ) ) { $this->rmdir( $dir ); - rmdir( $dir ); + $this->delete_folders( $dir ); } } } From 9807787e7df7341eb85d2ca32f9862a86c8c6fa2 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Fri, 30 Oct 2026 08:52:14 +0600 Subject: [PATCH 6/6] Tests: Verify rendered action links in plugins list table single_row(). --- .../admin/wpPluginsListTableActionLinks.php | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php index 859c92778ff5f..644c321388cae 100644 --- a/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php +++ b/tests/phpunit/tests/admin/wpPluginsListTableActionLinks.php @@ -47,6 +47,13 @@ class Tests_Admin_wpPluginsListTableActionLinks extends WP_PluginDependencies_Un */ private $actions; + /** + * The rendered HTML output of the plugin row. + * + * @var string + */ + private $row_output = ''; + /** * The slug of the plugin rendered by the current test. * @@ -115,8 +122,9 @@ public function set_up() { update_option( 'active_plugins', array() ); update_site_option( 'active_sitewide_plugins', array() ); - $this->actions = null; - $this->slug = 'action-links-plugin-' . wp_generate_password( 8, false ); + $this->actions = null; + $this->row_output = ''; + $this->slug = 'action-links-plugin-' . wp_generate_password( 8, false ); add_filter( 'plugin_action_links', array( $this, 'capture_actions' ), PHP_INT_MAX ); @@ -364,6 +372,16 @@ public function test_should_pass_plugin_and_context_to_filter() { $filter = new MockAction(); add_filter( 'plugin_action_links', array( $filter, 'filter' ), 10, 4 ); + $distinctive_link = 'Distinctive Site Link'; + add_filter( + 'plugin_action_links', + static function ( $actions ) use ( $distinctive_link ) { + $actions['distinctive'] = $distinctive_link; + return $actions; + }, + 20 + ); + $this->get_actions(); $args = $filter->get_args(); @@ -371,6 +389,12 @@ public function test_should_pass_plugin_and_context_to_filter() { $this->assertSame( $this->get_file(), $args[0][1], 'The filter should receive the plugin file.' ); $this->assertSame( 'My Plugin', $args[0][2]['Name'], 'The filter should receive the plugin data.' ); $this->assertSame( 'all', $args[0][3], 'The filter should receive the context.' ); + + $this->assertStringContainsString( + $distinctive_link, + $this->row_output, + 'The rendered row should contain action links added by filters.' + ); } /** @@ -415,7 +439,7 @@ private function get_actions( $plugin_data = array(), $headers = '' ) { $table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => 'plugins' ) ); - get_echo( + $this->row_output = get_echo( array( $table, 'single_row' ), array( array(