Skip to content

Commit 2a29ae9

Browse files
committed
Tests: Cover plugin dependency notices.
Add coverage for `WP_Plugin_Install_List_Table::get_dependencies_notice()`, including stored dependency data, API fallbacks, escaping, dependency order, and multisite URLs. Developed in: #14526 Props huzaifaalmesbah, mukesh27. See #65819. git-svn-id: https://develop.svn.wordpress.org/trunk@64629 602fd350-edb4-49c9-b593-d223f7449a82
1 parent ab6830a commit 2a29ae9

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
<?php
2+
3+
/**
4+
* Tests for WP_Plugin_Install_List_Table::get_dependencies_notice().
5+
*
6+
* @group admin
7+
* @group plugins
8+
*
9+
* @covers WP_Plugin_Install_List_Table::get_dependencies_notice
10+
* @covers WP_Plugin_Install_List_Table::get_more_details_link
11+
*/
12+
class Tests_Admin_wpPluginInstallListTableGetDependenciesNotice extends WP_UnitTestCase {
13+
14+
/**
15+
* The notice before the list of dependencies.
16+
*
17+
* @var string
18+
*/
19+
const NOTICE_START = '<div class="plugin-dependencies notice notice-alt notice-info inline"><p class="plugin-dependencies-explainer-text"><strong>Additional plugins are required</strong></p> ';
20+
21+
/**
22+
* The list table.
23+
*
24+
* @var WP_Plugin_Install_List_Table
25+
*/
26+
private $table;
27+
28+
/**
29+
* The original values of the globals and properties the tests change.
30+
*
31+
* @var array
32+
*/
33+
private $original = array();
34+
35+
/**
36+
* Slugs requested from the plugins API.
37+
*
38+
* @var string[]
39+
*/
40+
private $api_requests = array();
41+
42+
/**
43+
* Responses of the mocked plugins API, keyed by slug.
44+
*
45+
* @var array
46+
*/
47+
private $api_responses = array();
48+
49+
/**
50+
* Loads plugins_api(), which the Add Plugins screen loads.
51+
*/
52+
public static function set_up_before_class() {
53+
parent::set_up_before_class();
54+
55+
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
56+
}
57+
58+
/**
59+
* Sets up the list table, the Add Plugins screen and the plugins API mock.
60+
*/
61+
public function set_up() {
62+
parent::set_up();
63+
64+
$this->original = array(
65+
'pagenow' => $GLOBALS['pagenow'] ?? null,
66+
'current_screen' => $GLOBALS['current_screen'] ?? null,
67+
'dependency_api_data' => $this->get_dependency_api_data(),
68+
);
69+
70+
$this->table = _get_list_table( 'WP_Plugin_Install_List_Table', array( 'screen' => 'plugin-install' ) );
71+
72+
$GLOBALS['pagenow'] = 'plugin-install.php';
73+
set_current_screen( 'plugin-install' );
74+
$this->set_dependency_api_data( array() );
75+
76+
add_filter( 'plugins_api', array( $this, 'mock_plugins_api' ), 10, 3 );
77+
}
78+
79+
/**
80+
* Restores the globals and properties.
81+
*/
82+
public function tear_down() {
83+
$GLOBALS['pagenow'] = $this->original['pagenow'];
84+
$GLOBALS['current_screen'] = $this->original['current_screen'];
85+
$this->set_dependency_api_data( $this->original['dependency_api_data'] );
86+
87+
parent::tear_down();
88+
}
89+
90+
/**
91+
* Tests that no notice is returned for a plugin without dependencies.
92+
*
93+
* @ticket 65819
94+
*
95+
* @dataProvider data_no_dependencies
96+
*
97+
* @param array $plugin_data The plugin data.
98+
*/
99+
public function test_should_return_empty_string_without_dependencies( $plugin_data ) {
100+
$this->assertSame( '', $this->get_dependencies_notice( $plugin_data ) );
101+
}
102+
103+
/**
104+
* Data provider.
105+
*
106+
* @return array[]
107+
*/
108+
public static function data_no_dependencies() {
109+
return array(
110+
'no requires_plugins key' => array( array( 'slug' => 'my-plugin' ) ),
111+
'empty array' => array( array( 'requires_plugins' => array() ) ),
112+
'empty string' => array( array( 'requires_plugins' => '' ) ),
113+
);
114+
}
115+
116+
/**
117+
* Tests that a dependency is named from the stored dependency data.
118+
*
119+
* @ticket 65819
120+
*/
121+
public function test_should_name_dependency_from_dependency_data() {
122+
$this->set_dependency_api_data(
123+
array(
124+
'dep-one' => array(
125+
'name' => 'Dep <b>&</b> "One"',
126+
'slug' => 'dep-one',
127+
'version' => '1.0',
128+
),
129+
)
130+
);
131+
132+
$this->assertSame(
133+
self::NOTICE_START . $this->get_named_dependency( 'Dep &lt;b&gt;&amp;&lt;/b&gt; &quot;One&quot;', 'dep-one' ) . '</div>',
134+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ),
135+
'The dependency should be named from the stored data.'
136+
);
137+
$this->assertSame( array(), $this->api_requests, 'The plugins API should not be requested.' );
138+
}
139+
140+
/**
141+
* Tests that the details link uses the slug of the stored dependency data.
142+
*
143+
* @ticket 65819
144+
*/
145+
public function test_should_use_slug_from_dependency_data() {
146+
$this->set_dependency_api_data(
147+
array(
148+
'dep-one' => array(
149+
'name' => 'Dep One',
150+
'slug' => 'dep-one-renamed',
151+
'version' => '1.0',
152+
),
153+
)
154+
);
155+
156+
$this->assertSame(
157+
self::NOTICE_START . $this->get_named_dependency( 'Dep One', 'dep-one-renamed' ) . '</div>',
158+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) )
159+
);
160+
}
161+
162+
/**
163+
* Tests that incomplete dependency data falls back to the plugins API.
164+
*
165+
* @ticket 65819
166+
*
167+
* @dataProvider data_incomplete_dependency_data
168+
*
169+
* @param array|false $dependency_data The stored dependency data, or false for none.
170+
*/
171+
public function test_should_fall_back_to_plugins_api_for_incomplete_dependency_data( $dependency_data ) {
172+
if ( false !== $dependency_data ) {
173+
$this->set_dependency_api_data( array( 'dep-one' => $dependency_data ) );
174+
}
175+
176+
$this->api_responses['dep-one'] = (object) array(
177+
'name' => 'Dep One From API',
178+
'slug' => 'dep-one',
179+
);
180+
181+
$this->assertSame(
182+
self::NOTICE_START . $this->get_named_dependency( 'Dep One From API', 'dep-one' ) . '</div>',
183+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) ),
184+
'The dependency should be named from the plugins API.'
185+
);
186+
$this->assertSame( array( 'dep-one' ), $this->api_requests, 'The plugins API should be requested once.' );
187+
}
188+
189+
/**
190+
* Data provider.
191+
*
192+
* @return array[]
193+
*/
194+
public static function data_incomplete_dependency_data() {
195+
$complete = array(
196+
'name' => 'Dep One',
197+
'slug' => 'dep-one',
198+
'version' => '1.0',
199+
);
200+
201+
return array(
202+
'no dependency data' => array( false ),
203+
'empty name' => array( array_merge( $complete, array( 'name' => '' ) ) ),
204+
'empty slug' => array( array_merge( $complete, array( 'slug' => '' ) ) ),
205+
'empty version' => array( array_merge( $complete, array( 'version' => '' ) ) ),
206+
'no version' => array( array_diff_key( $complete, array( 'version' => true ) ) ),
207+
);
208+
}
209+
210+
/**
211+
* Tests that the details link uses the slug returned by the plugins API.
212+
*
213+
* @ticket 65819
214+
*/
215+
public function test_should_use_slug_from_plugins_api() {
216+
$this->api_responses['dep-one'] = (object) array(
217+
'name' => 'Dep <i>One</i>',
218+
'slug' => 'renamed-dep',
219+
);
220+
221+
$this->assertSame(
222+
self::NOTICE_START . $this->get_named_dependency( 'Dep &lt;i&gt;One&lt;/i&gt;', 'renamed-dep' ) . '</div>',
223+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one' ) ) )
224+
);
225+
}
226+
227+
/**
228+
* Tests that a dependency without a name is listed by its slug.
229+
*
230+
* @ticket 65819
231+
*
232+
* @dataProvider data_unnamed_api_responses
233+
*
234+
* @param string $response The plugins API response type.
235+
*/
236+
public function test_should_list_dependency_by_slug_without_name( $response ) {
237+
$this->api_responses['dep<one>'] = 'error' === $response
238+
? new WP_Error( 'plugins_api_failed', 'An error.' )
239+
: (object) array(
240+
'name' => '',
241+
'slug' => 'dep-one',
242+
);
243+
244+
$this->assertSame(
245+
self::NOTICE_START . '<div class="plugin-dependency"><span class="plugin-dependency-name">dep&lt;one&gt;</span></div></div>',
246+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep<one>' ) ) )
247+
);
248+
}
249+
250+
/**
251+
* Data provider.
252+
*
253+
* @return array[]
254+
*/
255+
public static function data_unnamed_api_responses() {
256+
return array(
257+
'API error' => array( 'error' ),
258+
'API data, no name' => array( 'no name' ),
259+
);
260+
}
261+
262+
/**
263+
* Tests that every dependency is listed, in order.
264+
*
265+
* @ticket 65819
266+
*/
267+
public function test_should_list_all_dependencies_in_order() {
268+
$this->set_dependency_api_data(
269+
array(
270+
'dep-one' => array(
271+
'name' => 'Dep One',
272+
'slug' => 'dep-one',
273+
'version' => '1.0',
274+
),
275+
)
276+
);
277+
278+
$this->api_responses['dep-two'] = (object) array(
279+
'name' => 'Dep Two',
280+
'slug' => 'dep-two',
281+
);
282+
$this->api_responses['dep-three'] = new WP_Error( 'plugins_api_failed', 'An error.' );
283+
284+
$this->assertSame(
285+
self::NOTICE_START
286+
. $this->get_named_dependency( 'Dep One', 'dep-one' )
287+
. $this->get_named_dependency( 'Dep Two', 'dep-two' )
288+
. '<div class="plugin-dependency"><span class="plugin-dependency-name">dep-three</span></div>'
289+
. '</div>',
290+
$this->get_dependencies_notice( array( 'requires_plugins' => array( 'dep-one', 'dep-two', 'dep-three' ) ) ),
291+
'Each dependency should be listed in order.'
292+
);
293+
$this->assertSame( array( 'dep-two', 'dep-three' ), $this->api_requests, 'Only dependencies without stored data should be requested.' );
294+
}
295+
296+
/**
297+
* Returns the expected markup of a named dependency.
298+
*
299+
* @param string $escaped_name The escaped plugin name.
300+
* @param string $slug The plugin slug.
301+
* @return string The markup.
302+
*/
303+
private function get_named_dependency( $escaped_name, $slug ) {
304+
$url = 'http://' . WP_TESTS_DOMAIN . '/wp-admin/' . ( is_multisite() ? 'network/' : '' )
305+
. 'plugin-install.php?tab=plugin-information&#038;plugin=' . $slug . '&#038;TB_iframe=true&#038;width=600&#038;height=550';
306+
307+
return '<div class="plugin-dependency"><span class="plugin-dependency-name">' . $escaped_name . '</span> '
308+
. '<a href="' . $url . '" class="more-details-link thickbox open-plugin-details-modal" aria-label="More information about ' . $escaped_name . '" data-title="' . $escaped_name . '">More Details</a>'
309+
. '</div>';
310+
}
311+
312+
/**
313+
* Calls the protected get_dependencies_notice() method.
314+
*
315+
* @param array $plugin_data The plugin data.
316+
* @return string The notice.
317+
*/
318+
private function get_dependencies_notice( $plugin_data ) {
319+
$method = new ReflectionMethod( $this->table, 'get_dependencies_notice' );
320+
if ( PHP_VERSION_ID < 80100 ) {
321+
$method->setAccessible( true );
322+
}
323+
324+
return $method->invoke( $this->table, $plugin_data );
325+
}
326+
327+
/**
328+
* Gets the stored dependency data of WP_Plugin_Dependencies.
329+
*
330+
* @return array|null The dependency data.
331+
*/
332+
private function get_dependency_api_data() {
333+
$property = new ReflectionProperty( 'WP_Plugin_Dependencies', 'dependency_api_data' );
334+
if ( PHP_VERSION_ID < 80100 ) {
335+
$property->setAccessible( true );
336+
}
337+
338+
return $property->getValue();
339+
}
340+
341+
/**
342+
* Sets the stored dependency data of WP_Plugin_Dependencies.
343+
*
344+
* @param array|null $value The dependency data.
345+
*/
346+
private function set_dependency_api_data( $value ) {
347+
$property = new ReflectionProperty( 'WP_Plugin_Dependencies', 'dependency_api_data' );
348+
if ( PHP_VERSION_ID < 80100 ) {
349+
$property->setAccessible( true );
350+
}
351+
352+
$property->setValue( null, $value );
353+
}
354+
355+
/**
356+
* Returns the mocked plugins API response for the requested slug.
357+
*
358+
* @param false|object|array $result The result object or array.
359+
* @param string $action The type of information being requested.
360+
* @param object $args Plugin API arguments.
361+
* @return object|WP_Error The mocked response.
362+
*/
363+
public function mock_plugins_api( $result, $action, $args ) {
364+
$this->api_requests[] = $args->slug;
365+
366+
return $this->api_responses[ $args->slug ] ?? new WP_Error( 'plugins_api_failed', 'No mocked response.' );
367+
}
368+
}

0 commit comments

Comments
 (0)