PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / Integration / Toolbar / FrontendTest.php

FrontendTest.php in Extendify 3.1.0, at tests/Integration/Toolbar/FrontendTest.php

431 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\Tests\Integration\Toolbar;
4
5 use Extendify\Config;
6 use Extendify\PartnerData;
7 use Extendify\Toolbar\Frontend;
8 use WP_UnitTestCase;
9
10 /**
11 * Characterizes the render-decision truth table for the simple
12 * front-end toolbar.
13 *
14 * Findings pinned:
15 *
16 * - shouldRender() gates on `edit_posts`, matching the QuickEdit
17 * pill (not `manage_options`).
18 * - The Launch-aware default (`defaultStyle()`) flips on
19 * `Config::$launchCompleted`. Pre-Launch the toolbar never
20 * renders by default — it returns 'full' and the style check
21 * fails. Post-Launch it returns 'simple' and the bar appears.
22 * - The core admin bar is left in the DOM and hidden via CSS so
23 * the Agent's mounted button still exists for the toolbar's
24 * "AI Agent" link to drive. `hideCoreAdminBar()` emits the
25 * reservation CSS only when `shouldRender()` is true.
26 * - `loadScriptsAndStyles` aborts when the asset manifest is
27 * missing the entry — useful failure mode for a fresh checkout
28 * that hasn't been built yet.
29 */
30 class FrontendTest extends WP_UnitTestCase
31 {
32 public static function setUpBeforeClass(): void
33 {
34 parent::setUpBeforeClass();
35 if (!defined('EXTENDIFY_REQUIRED_CAPABILITY')) {
36 define('EXTENDIFY_REQUIRED_CAPABILITY', 'manage_options');
37 }
38 if (!defined('EXTENDIFY_DEVMODE')) {
39 define('EXTENDIFY_DEVMODE', false);
40 }
41 if (!defined('EXTENDIFY_PATH')) {
42 define('EXTENDIFY_PATH', dirname(__DIR__, 3) . '/');
43 }
44 if (!defined('EXTENDIFY_BASE_URL')) {
45 define('EXTENDIFY_BASE_URL', 'http://example.org/wp-content/plugins/extendify-sdk/');
46 }
47 }
48
49 public function tearDown(): void
50 {
51 remove_all_actions('wp_enqueue_scripts');
52 remove_all_actions('wp_body_open');
53 remove_all_actions('wp_head');
54 wp_dequeue_script('extendify-toolbar-scripts');
55 wp_deregister_script('extendify-toolbar-scripts');
56 wp_dequeue_style('extendify-toolbar-styles');
57 wp_deregister_style('extendify-toolbar-styles');
58 Config::$assetManifest = [];
59 Config::$launchCompleted = false;
60 // Static PartnerData::$config leaks across tests/classes.
61 $this->setPartnerSetting('showQuickEdit', false);
62 set_current_screen('front');
63 unset($GLOBALS['wp_customize']);
64 parent::tearDown();
65 }
66
67 public function test_constructor_hooks_three_actions()
68 {
69 $frontend = new Frontend();
70
71 $this->assertNotFalse(has_action('wp_enqueue_scripts', [$frontend, 'loadScriptsAndStyles']));
72 $this->assertNotFalse(has_action('wp_body_open', [$frontend, 'render']));
73 $this->assertNotFalse(has_action('wp_head', [$frontend, 'hideCoreAdminBar']));
74 }
75
76 public function test_default_style_is_full_pre_launch()
77 {
78 Config::$launchCompleted = false;
79 $this->assertSame('full', Frontend::defaultStyle());
80 }
81
82 public function test_default_style_is_simple_post_launch()
83 {
84 Config::$launchCompleted = true;
85 $this->assertSame('simple', Frontend::defaultStyle());
86 }
87
88 public function test_style_returns_default_when_user_has_not_picked()
89 {
90 Config::$launchCompleted = true;
91 $userId = self::factory()->user->create(['role' => 'editor']);
92 delete_user_meta($userId, Frontend::STYLE_META);
93
94 $this->assertSame('simple', Frontend::style($userId));
95 }
96
97 public function test_style_honors_user_meta_over_default()
98 {
99 Config::$launchCompleted = true;
100 $userId = self::factory()->user->create(['role' => 'editor']);
101 update_user_meta($userId, Frontend::STYLE_META, 'full');
102
103 $this->assertSame('full', Frontend::style($userId));
104 }
105
106 public function test_style_ignores_invalid_meta_values()
107 {
108 Config::$launchCompleted = false;
109 $userId = self::factory()->user->create(['role' => 'editor']);
110 update_user_meta($userId, Frontend::STYLE_META, 'garbage');
111
112 $this->assertSame('full', Frontend::style($userId));
113 }
114
115 public function test_style_falls_back_to_default_when_no_user_in_context()
116 {
117 Config::$launchCompleted = true;
118 wp_set_current_user(0);
119
120 $this->assertSame('simple', Frontend::style());
121 }
122
123 public function test_should_render_false_in_admin_context()
124 {
125 $this->primeRenderable();
126 set_current_screen('edit-post');
127
128 $this->assertFalse(Frontend::shouldRender());
129 }
130
131 public function test_should_render_false_in_customize_preview()
132 {
133 // The Customizer preview iframe renders the front end for an admin —
134 // only is_customize_preview() distinguishes it from the live page, so
135 // the simple toolbar must not render there.
136 $this->primeRenderableSimple();
137 require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php';
138 $GLOBALS['wp_customize'] = new \WP_Customize_Manager();
139 $GLOBALS['wp_customize']->start_previewing_theme();
140
141 $this->assertFalse(Frontend::shouldRender());
142 }
143
144 public function test_should_render_false_when_logged_out()
145 {
146 $this->primeRenderable();
147 wp_set_current_user(0);
148
149 $this->assertFalse(Frontend::shouldRender());
150 }
151
152 public function test_should_render_false_for_subscriber_lacking_required_capability()
153 {
154 $this->primeRenderable();
155 $userId = self::factory()->user->create(['role' => 'subscriber']);
156 wp_set_current_user($userId);
157 update_user_option($userId, 'show_admin_bar_front', 'true');
158
159 $this->assertFalse(Frontend::shouldRender());
160 }
161
162 public function test_should_render_false_for_editor_lacking_manage_options()
163 {
164 // Locked down: shouldRender gates on Config::$requiredCapability
165 // (default manage_options). Editors have edit_posts but not
166 // manage_options, so the simple toolbar must NOT render for them.
167 $this->primeRenderable();
168 $userId = self::factory()->user->create(['role' => 'editor']);
169 wp_set_current_user($userId);
170 update_user_option($userId, 'show_admin_bar_front', 'true');
171 update_user_meta($userId, Frontend::STYLE_META, 'simple');
172
173 $this->assertFalse(Frontend::shouldRender());
174 }
175
176 public function test_should_render_false_when_core_show_admin_bar_pref_off()
177 {
178 $userId = $this->primeRenderable();
179 update_user_option($userId, 'show_admin_bar_front', 'false');
180
181 $this->assertFalse(Frontend::shouldRender());
182 }
183
184 public function test_should_render_false_when_style_resolves_to_full()
185 {
186 $userId = $this->primeRenderable();
187 update_user_meta($userId, Frontend::STYLE_META, 'full');
188
189 $this->assertFalse(Frontend::shouldRender());
190 }
191
192 public function test_should_render_true_for_administrator_with_simple_style()
193 {
194 $userId = $this->primeRenderable();
195 update_user_meta($userId, Frontend::STYLE_META, 'simple');
196
197 $this->assertTrue(Frontend::shouldRender());
198 }
199
200 public function test_load_scripts_and_styles_skips_when_should_render_false()
201 {
202 wp_set_current_user(0);
203
204 (new Frontend())->loadScriptsAndStyles();
205
206 $this->assertFalse(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
207 $this->assertFalse(wp_style_is('extendify-toolbar-styles', 'enqueued'));
208 }
209
210 public function test_load_scripts_and_styles_skips_when_manifest_missing_entry()
211 {
212 $this->primeRenderableSimple();
213 Config::$assetManifest = [];
214
215 (new Frontend())->loadScriptsAndStyles();
216
217 $this->assertFalse(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
218 }
219
220 public function test_load_scripts_and_styles_enqueues_when_renderable_with_manifest()
221 {
222 $this->primeRenderableSimple();
223 $this->setManifestEntries(['extendify-toolbar.php', 'extendify-toolbar.js']);
224
225 (new Frontend())->loadScriptsAndStyles();
226
227 $this->assertTrue(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
228 }
229
230 public function test_load_scripts_and_styles_enqueues_css_when_manifest_includes_it()
231 {
232 $this->primeRenderableSimple();
233 $this->setManifestEntries([
234 'extendify-toolbar.php',
235 'extendify-toolbar.js',
236 'extendify-toolbar.css',
237 ]);
238
239 (new Frontend())->loadScriptsAndStyles();
240
241 $this->assertTrue(wp_style_is('extendify-toolbar-styles', 'enqueued'));
242 }
243
244 public function test_render_outputs_nothing_when_should_render_false()
245 {
246 wp_set_current_user(0);
247
248 $html = $this->capture(fn () => (new Frontend())->render());
249
250 $this->assertSame('', trim($html));
251 }
252
253 public function test_render_emits_nothing_for_logged_in_subscriber()
254 {
255 // Front-end exposure: a logged-in under-priv viewer can
256 // still browse the public page; the toolbar markup (and the wp-admin
257 // link / queried post-edit URL in it) must never reach them. The
258 // logged-out test above passes even if only the cap check were
259 // dropped, so pin the render() sink directly for a logged-in
260 // below-cap user.
261 $this->primeRenderableSimple();
262 $userId = self::factory()->user->create(['role' => 'subscriber']);
263 wp_set_current_user($userId);
264 update_user_option($userId, 'show_admin_bar_front', 'true');
265 update_user_meta($userId, Frontend::STYLE_META, 'simple');
266
267 $html = $this->capture(fn () => (new Frontend())->render());
268
269 $this->assertSame('', trim($html));
270 }
271
272 public function test_render_emits_nothing_for_editor_lacking_required_capability()
273 {
274 // Same exposure check one rung up: an editor has
275 // edit_posts but not the default manage_options gate, so the simple
276 // toolbar must not render (or leak its markup) for them either.
277 $this->primeRenderableSimple();
278 $userId = self::factory()->user->create(['role' => 'editor']);
279 wp_set_current_user($userId);
280 update_user_option($userId, 'show_admin_bar_front', 'true');
281 update_user_meta($userId, Frontend::STYLE_META, 'simple');
282
283 $html = $this->capture(fn () => (new Frontend())->render());
284
285 $this->assertSame('', trim($html));
286 }
287
288 public function test_toolbar_does_not_remove_core_admin_bar_from_dom()
289 {
290 // Replacing the admin bar must not drop
291 // a security-relevant core control. The toolbar hides #wpadminbar via
292 // CSS only (display:none), leaving the core bar enabled so its logout
293 // and nonce'd controls stay in the DOM (and the Agent's mounted button
294 // stays present for the toolbar's "AI Agent" link to drive). Pin that
295 // we never suppress the bar via the show_admin_bar filter — a
296 // regression adding __return_false there would turn this red.
297 $this->primeRenderableSimple();
298
299 new Frontend();
300
301 $this->assertTrue((bool) apply_filters('show_admin_bar', true));
302 }
303
304 public function test_render_outputs_toolbar_without_quick_edit_button_when_flag_off()
305 {
306 // showQuickEdit defaults off → the Edit-mode toggle is omitted, but
307 // the rest of the simple toolbar (AI Agent, WP Admin) still renders.
308 $this->primeRenderableSimple();
309
310 $html = $this->capture(fn () => (new Frontend())->render());
311
312 $this->assertStringContainsString('id="extendify-toolbar"', $html);
313 $this->assertStringContainsString('id="ext-tb-ai-agent"', $html);
314 $this->assertStringContainsString('WP Admin', $html);
315 $this->assertStringNotContainsString('id="ext-tb-quick-edit"', $html);
316 }
317
318 public function test_render_includes_quick_edit_button_when_show_quick_edit_on()
319 {
320 $this->primeRenderableSimple();
321 $this->setPartnerSetting('showQuickEdit', true);
322
323 $html = $this->capture(fn () => (new Frontend())->render());
324
325 $this->assertStringContainsString('id="ext-tb-quick-edit"', $html);
326 $this->assertStringContainsString('role="switch"', $html);
327 $this->assertStringContainsString('aria-checked="false"', $html);
328 }
329
330 public function test_render_emits_post_edit_link_when_queried_object_is_a_post()
331 {
332 $this->primeRenderableSimple('administrator');
333 $postId = self::factory()->post->create(['post_status' => 'publish']);
334 global $wp_query;
335 $wp_query->queried_object = get_post($postId);
336 $wp_query->queried_object_id = $postId;
337
338 $html = $this->capture(fn () => (new Frontend())->render());
339
340 $this->assertStringContainsString('ext-tb-edit', $html);
341 $this->assertStringContainsString('post.php?post=' . $postId, $html);
342 }
343
344 public function test_render_omits_post_edit_link_when_no_post_in_context()
345 {
346 $this->primeRenderableSimple();
347 global $wp_query;
348 $wp_query->queried_object = null;
349
350 $html = $this->capture(fn () => (new Frontend())->render());
351
352 $this->assertStringContainsString('id="extendify-toolbar"', $html);
353 $this->assertStringNotContainsString('ext-tb-edit', $html);
354 }
355
356 public function test_hide_core_admin_bar_emits_reset_css_when_renderable()
357 {
358 $this->primeRenderableSimple();
359
360 $html = $this->capture(fn () => (new Frontend())->hideCoreAdminBar());
361
362 $this->assertStringContainsString('id="extendify-toolbar-reset"', $html);
363 $this->assertStringContainsString('#wpadminbar { display: none', $html);
364 $this->assertStringContainsString('margin-top: 32px', $html);
365 }
366
367 public function test_hide_core_admin_bar_emits_nothing_when_not_renderable()
368 {
369 wp_set_current_user(0);
370
371 $html = $this->capture(fn () => (new Frontend())->hideCoreAdminBar());
372
373 $this->assertSame('', trim($html));
374 }
375
376 /**
377 * Set up a logged-in user with the core "show admin bar on front"
378 * pref turned on, front-end screen, and `Config::$launchCompleted`
379 * = true so `defaultStyle()` would return 'simple'.
380 *
381 * Returns the user id so the test can layer meta or further setup
382 * on top.
383 */
384 private function primeRenderable(string $role = 'administrator'): int
385 {
386 Config::$launchCompleted = true;
387 $userId = self::factory()->user->create(['role' => $role]);
388 wp_set_current_user($userId);
389 update_user_option($userId, 'show_admin_bar_front', 'true');
390 set_current_screen('front');
391 return $userId;
392 }
393
394 /**
395 * Same as primeRenderable plus an explicit 'simple' user meta so
396 * style() resolves to 'simple' even if a future change makes the
397 * Launch-aware default less reliable.
398 */
399 private function primeRenderableSimple(string $role = 'administrator'): int
400 {
401 $userId = $this->primeRenderable($role);
402 update_user_meta($userId, Frontend::STYLE_META, 'simple');
403 return $userId;
404 }
405
406 private function setPartnerSetting(string $key, $value): void
407 {
408 $prop = new \ReflectionProperty(PartnerData::class, 'config');
409 $prop->setAccessible(true);
410 $config = $prop->getValue();
411 $config[$key] = $value;
412 $prop->setValue(null, $config);
413 }
414
415 private function setManifestEntries(array $keys): void
416 {
417 $manifest = [];
418 foreach ($keys as $key) {
419 $manifest[$key] = $key;
420 }
421 Config::$assetManifest = $manifest;
422 }
423
424 private function capture(callable $fn): string
425 {
426 ob_start();
427 $fn();
428 return (string) ob_get_clean();
429 }
430 }
431