Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
324 changes: 324 additions & 0 deletions tests/phpunit/tests/admin/wpPluginsListTableRowMeta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
<?php

/**
* Tests for the version, author and details links WP_Plugins_List_Table::single_row() shows under a plugin.
*
* @group admin
* @group plugins
*
* @covers WP_Plugins_List_Table::single_row
*/
class Tests_Admin_wpPluginsListTableRowMeta extends WP_UnitTestCase {

/**
* Administrator user ID, a super admin on multisite.
*
* @var int
*/
private static $admin_id;

/**
* ID of a user who can activate but not install plugins.
*
* @var int
*/
private static $activator_id;

/**
* Number used to give each rendered plugin a unique file.
*
* @var int
*/
private static $counter = 0;

/**
* The original globals the list table reads.
*
* @var array
*/
private $original_globals = array();

/**
* The arguments passed to the plugin_row_meta filter.
*
* @var array|null
*/
private $filter_args;

/**
* The slug of the plugin rendered by the current test.
*
* @var string
*/
private $slug;

/**
* Sets up the users.
*
* @param WP_UnitTest_Factory $factory Test factory.
*/
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$admin_id = $factory->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;
}

$this->remove_plugin();

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 <a href="https://example.org/jane/">Jane</a>',
'<a href="http://' . WP_TESTS_DOMAIN . '/wp-admin/' . ( is_multisite() ? 'network/' : '' ) . 'plugin-install.php?tab=plugin-information&#038;plugin=' . $this->slug . '&#038;TB_iframe=true&#038;width=600&#038;height=550"'
. ' class="thickbox open-plugin-details-modal" aria-label="More information about My &quot;Plugin&quot;" data-title="My &quot;Plugin&quot;">View details</a>',
),
$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',
'<a href="https://example.org/plugin/?a=1&#038;b=2" aria-label="Visit plugin site for My Plugin">Visit plugin site</a>',
),
$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( '<a href="https://example.org/plugin/" aria-label="Visit plugin site for My Plugin">Visit plugin site</a>' ),
$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[] = '<a href="#">Docs</a>';

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 | <a href="#">Docs</a></div>', $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->remove_plugin();
$this->assertTrue( mkdir( $dir ), 'The test plugin directory should be created.' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In core tests, WP_PLUGIN_DIR is tests/phpunit/data/plugins, which is a tracked directory. If a run stops partway, such as on a fatal error or Ctrl+C, tear_down() never runs and this fixture folder stays there. The slug comes from a counter that starts at 1 again on the next run, so mkdir() fails on the same folder name. So every test in the class fails here until someone deletes the folder by hand. The leftover also shows up in git status.

Could this helper, or set_up(), remove a leftover folder first? #14600 already does this: its remove_plugin() helper runs in both set_up() and tear_down().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! Added a remove_plugin() helper that clears a leftover folder before mkdir() and in tear_down(), in 4f70c2e. I checked it with a leftover folder in place: the old code failed and the new code passes.

$this->assertNotFalse( file_put_contents( $dir . '/' . $this->slug . '.php', "<?php\n/**\n * Plugin Name: My Plugin\n */\n" ), 'The test plugin should be written.' );

$table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => '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";
}

/**
* 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 );
}
}
}
Loading