HEX
Server: LiteSpeed
System: Linux server342.web-hosting.com 4.18.0-553.124.4.lve.el8.x86_64 #1 SMP Fri May 15 13:02:13 UTC 2026 x86_64
User: ksonpoau (1099)
PHP: 8.2.31
Disabled: NONE
Upload Files
File: //home/ksonpoau/www/wp-content/plugins/extendify/tests/Integration/Toolbar/FrontendTest.php
<?php

namespace Extendify\Tests\Integration\Toolbar;

use Extendify\Config;
use Extendify\PartnerData;
use Extendify\Toolbar\Frontend;
use WP_UnitTestCase;

/**
 * Characterizes the render-decision truth table for the simple
 * front-end toolbar.
 *
 * Findings pinned:
 *
 *  - shouldRender() gates on `edit_posts`, matching the QuickEdit
 *    pill (not `manage_options`).
 *  - The Launch-aware default (`defaultStyle()`) flips on
 *    `Config::$launchCompleted`. Pre-Launch the toolbar never
 *    renders by default — it returns 'full' and the style check
 *    fails. Post-Launch it returns 'simple' and the bar appears.
 *  - The core admin bar is left in the DOM and hidden via CSS so
 *    the Agent's mounted button still exists for the toolbar's
 *    "AI Agent" link to drive. `hideCoreAdminBar()` emits the
 *    reservation CSS only when `shouldRender()` is true.
 *  - `loadScriptsAndStyles` aborts when the asset manifest is
 *    missing the entry — useful failure mode for a fresh checkout
 *    that hasn't been built yet.
 */
class FrontendTest extends WP_UnitTestCase
{
    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();
        if (!defined('EXTENDIFY_REQUIRED_CAPABILITY')) {
            define('EXTENDIFY_REQUIRED_CAPABILITY', 'manage_options');
        }
        if (!defined('EXTENDIFY_DEVMODE')) {
            define('EXTENDIFY_DEVMODE', false);
        }
        if (!defined('EXTENDIFY_PATH')) {
            define('EXTENDIFY_PATH', dirname(__DIR__, 3) . '/');
        }
        if (!defined('EXTENDIFY_BASE_URL')) {
            define('EXTENDIFY_BASE_URL', 'http://example.org/wp-content/plugins/extendify-sdk/');
        }
    }

    public function tearDown(): void
    {
        remove_all_actions('wp_enqueue_scripts');
        remove_all_actions('wp_body_open');
        remove_all_actions('wp_head');
        wp_dequeue_script('extendify-toolbar-scripts');
        wp_deregister_script('extendify-toolbar-scripts');
        wp_dequeue_style('extendify-toolbar-styles');
        wp_deregister_style('extendify-toolbar-styles');
        Config::$assetManifest = [];
        Config::$launchCompleted = false;
        // Static PartnerData::$config leaks across tests/classes.
        $this->setPartnerSetting('showQuickEdit', false);
        set_current_screen('front');
        unset($GLOBALS['wp_customize']);
        parent::tearDown();
    }

    public function test_constructor_hooks_three_actions()
    {
        $frontend = new Frontend();

        $this->assertNotFalse(has_action('wp_enqueue_scripts', [$frontend, 'loadScriptsAndStyles']));
        $this->assertNotFalse(has_action('wp_body_open', [$frontend, 'render']));
        $this->assertNotFalse(has_action('wp_head', [$frontend, 'hideCoreAdminBar']));
    }

    public function test_default_style_is_full_pre_launch()
    {
        Config::$launchCompleted = false;
        $this->assertSame('full', Frontend::defaultStyle());
    }

    public function test_default_style_is_simple_post_launch()
    {
        Config::$launchCompleted = true;
        $this->assertSame('simple', Frontend::defaultStyle());
    }

    public function test_style_returns_default_when_user_has_not_picked()
    {
        Config::$launchCompleted = true;
        $userId = self::factory()->user->create(['role' => 'editor']);
        delete_user_meta($userId, Frontend::STYLE_META);

        $this->assertSame('simple', Frontend::style($userId));
    }

    public function test_style_honors_user_meta_over_default()
    {
        Config::$launchCompleted = true;
        $userId = self::factory()->user->create(['role' => 'editor']);
        update_user_meta($userId, Frontend::STYLE_META, 'full');

        $this->assertSame('full', Frontend::style($userId));
    }

    public function test_style_ignores_invalid_meta_values()
    {
        Config::$launchCompleted = false;
        $userId = self::factory()->user->create(['role' => 'editor']);
        update_user_meta($userId, Frontend::STYLE_META, 'garbage');

        $this->assertSame('full', Frontend::style($userId));
    }

    public function test_style_falls_back_to_default_when_no_user_in_context()
    {
        Config::$launchCompleted = true;
        wp_set_current_user(0);

        $this->assertSame('simple', Frontend::style());
    }

    public function test_should_render_false_in_admin_context()
    {
        $this->primeRenderable();
        set_current_screen('edit-post');

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_in_customize_preview()
    {
        // The Customizer preview iframe renders the front end for an admin —
        // only is_customize_preview() distinguishes it from the live page, so
        // the simple toolbar must not render there.
        $this->primeRenderableSimple();
        require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php';
        $GLOBALS['wp_customize'] = new \WP_Customize_Manager();
        $GLOBALS['wp_customize']->start_previewing_theme();

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_when_logged_out()
    {
        $this->primeRenderable();
        wp_set_current_user(0);

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_for_subscriber_lacking_required_capability()
    {
        $this->primeRenderable();
        $userId = self::factory()->user->create(['role' => 'subscriber']);
        wp_set_current_user($userId);
        update_user_option($userId, 'show_admin_bar_front', 'true');

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_for_editor_lacking_manage_options()
    {
        // Locked down: shouldRender gates on Config::$requiredCapability
        // (default manage_options). Editors have edit_posts but not
        // manage_options, so the simple toolbar must NOT render for them.
        $this->primeRenderable();
        $userId = self::factory()->user->create(['role' => 'editor']);
        wp_set_current_user($userId);
        update_user_option($userId, 'show_admin_bar_front', 'true');
        update_user_meta($userId, Frontend::STYLE_META, 'simple');

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_when_core_show_admin_bar_pref_off()
    {
        $userId = $this->primeRenderable();
        update_user_option($userId, 'show_admin_bar_front', 'false');

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_false_when_style_resolves_to_full()
    {
        $userId = $this->primeRenderable();
        update_user_meta($userId, Frontend::STYLE_META, 'full');

        $this->assertFalse(Frontend::shouldRender());
    }

    public function test_should_render_true_for_administrator_with_simple_style()
    {
        $userId = $this->primeRenderable();
        update_user_meta($userId, Frontend::STYLE_META, 'simple');

        $this->assertTrue(Frontend::shouldRender());
    }

    public function test_load_scripts_and_styles_skips_when_should_render_false()
    {
        wp_set_current_user(0);

        (new Frontend())->loadScriptsAndStyles();

        $this->assertFalse(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
        $this->assertFalse(wp_style_is('extendify-toolbar-styles', 'enqueued'));
    }

    public function test_load_scripts_and_styles_skips_when_manifest_missing_entry()
    {
        $this->primeRenderableSimple();
        Config::$assetManifest = [];

        (new Frontend())->loadScriptsAndStyles();

        $this->assertFalse(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
    }

    public function test_load_scripts_and_styles_enqueues_when_renderable_with_manifest()
    {
        $this->primeRenderableSimple();
        $this->setManifestEntries(['extendify-toolbar.php', 'extendify-toolbar.js']);

        (new Frontend())->loadScriptsAndStyles();

        $this->assertTrue(wp_script_is('extendify-toolbar-scripts', 'enqueued'));
    }

    public function test_load_scripts_and_styles_enqueues_css_when_manifest_includes_it()
    {
        $this->primeRenderableSimple();
        $this->setManifestEntries([
            'extendify-toolbar.php',
            'extendify-toolbar.js',
            'extendify-toolbar.css',
        ]);

        (new Frontend())->loadScriptsAndStyles();

        $this->assertTrue(wp_style_is('extendify-toolbar-styles', 'enqueued'));
    }

    public function test_render_outputs_nothing_when_should_render_false()
    {
        wp_set_current_user(0);

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertSame('', trim($html));
    }

    public function test_render_emits_nothing_for_logged_in_subscriber()
    {
        // Front-end exposure: a logged-in under-priv viewer can
        // still browse the public page; the toolbar markup (and the wp-admin
        // link / queried post-edit URL in it) must never reach them. The
        // logged-out test above passes even if only the cap check were
        // dropped, so pin the render() sink directly for a logged-in
        // below-cap user.
        $this->primeRenderableSimple();
        $userId = self::factory()->user->create(['role' => 'subscriber']);
        wp_set_current_user($userId);
        update_user_option($userId, 'show_admin_bar_front', 'true');
        update_user_meta($userId, Frontend::STYLE_META, 'simple');

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertSame('', trim($html));
    }

    public function test_render_emits_nothing_for_editor_lacking_required_capability()
    {
        // Same exposure check one rung up: an editor has
        // edit_posts but not the default manage_options gate, so the simple
        // toolbar must not render (or leak its markup) for them either.
        $this->primeRenderableSimple();
        $userId = self::factory()->user->create(['role' => 'editor']);
        wp_set_current_user($userId);
        update_user_option($userId, 'show_admin_bar_front', 'true');
        update_user_meta($userId, Frontend::STYLE_META, 'simple');

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertSame('', trim($html));
    }

    public function test_toolbar_does_not_remove_core_admin_bar_from_dom()
    {
        // Replacing the admin bar must not drop
        // a security-relevant core control. The toolbar hides #wpadminbar via
        // CSS only (display:none), leaving the core bar enabled so its logout
        // and nonce'd controls stay in the DOM (and the Agent's mounted button
        // stays present for the toolbar's "AI Agent" link to drive). Pin that
        // we never suppress the bar via the show_admin_bar filter — a
        // regression adding __return_false there would turn this red.
        $this->primeRenderableSimple();

        new Frontend();

        $this->assertTrue((bool) apply_filters('show_admin_bar', true));
    }

    public function test_render_outputs_toolbar_without_quick_edit_button_when_flag_off()
    {
        // showQuickEdit defaults off → the Edit-mode toggle is omitted, but
        // the rest of the simple toolbar (AI Agent, WP Admin) still renders.
        $this->primeRenderableSimple();

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertStringContainsString('id="extendify-toolbar"', $html);
        $this->assertStringContainsString('id="ext-tb-ai-agent"', $html);
        $this->assertStringContainsString('WP Admin', $html);
        $this->assertStringNotContainsString('id="ext-tb-quick-edit"', $html);
    }

    public function test_render_includes_quick_edit_button_when_show_quick_edit_on()
    {
        $this->primeRenderableSimple();
        $this->setPartnerSetting('showQuickEdit', true);

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertStringContainsString('id="ext-tb-quick-edit"', $html);
        $this->assertStringContainsString('role="switch"', $html);
        $this->assertStringContainsString('aria-checked="false"', $html);
    }

    public function test_render_emits_post_edit_link_when_queried_object_is_a_post()
    {
        $this->primeRenderableSimple('administrator');
        $postId = self::factory()->post->create(['post_status' => 'publish']);
        global $wp_query;
        $wp_query->queried_object = get_post($postId);
        $wp_query->queried_object_id = $postId;

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertStringContainsString('ext-tb-edit', $html);
        $this->assertStringContainsString('post.php?post=' . $postId, $html);
    }

    public function test_render_omits_post_edit_link_when_no_post_in_context()
    {
        $this->primeRenderableSimple();
        global $wp_query;
        $wp_query->queried_object = null;

        $html = $this->capture(fn () => (new Frontend())->render());

        $this->assertStringContainsString('id="extendify-toolbar"', $html);
        $this->assertStringNotContainsString('ext-tb-edit', $html);
    }

    public function test_hide_core_admin_bar_emits_reset_css_when_renderable()
    {
        $this->primeRenderableSimple();

        $html = $this->capture(fn () => (new Frontend())->hideCoreAdminBar());

        $this->assertStringContainsString('id="extendify-toolbar-reset"', $html);
        $this->assertStringContainsString('#wpadminbar { display: none', $html);
        $this->assertStringContainsString('margin-top: 32px', $html);
    }

    public function test_hide_core_admin_bar_emits_nothing_when_not_renderable()
    {
        wp_set_current_user(0);

        $html = $this->capture(fn () => (new Frontend())->hideCoreAdminBar());

        $this->assertSame('', trim($html));
    }

    /**
     * Set up a logged-in user with the core "show admin bar on front"
     * pref turned on, front-end screen, and `Config::$launchCompleted`
     * = true so `defaultStyle()` would return 'simple'.
     *
     * Returns the user id so the test can layer meta or further setup
     * on top.
     */
    private function primeRenderable(string $role = 'administrator'): int
    {
        Config::$launchCompleted = true;
        $userId = self::factory()->user->create(['role' => $role]);
        wp_set_current_user($userId);
        update_user_option($userId, 'show_admin_bar_front', 'true');
        set_current_screen('front');
        return $userId;
    }

    /**
     * Same as primeRenderable plus an explicit 'simple' user meta so
     * style() resolves to 'simple' even if a future change makes the
     * Launch-aware default less reliable.
     */
    private function primeRenderableSimple(string $role = 'administrator'): int
    {
        $userId = $this->primeRenderable($role);
        update_user_meta($userId, Frontend::STYLE_META, 'simple');
        return $userId;
    }

    private function setPartnerSetting(string $key, $value): void
    {
        $prop = new \ReflectionProperty(PartnerData::class, 'config');
        $prop->setAccessible(true);
        $config = $prop->getValue();
        $config[$key] = $value;
        $prop->setValue(null, $config);
    }

    private function setManifestEntries(array $keys): void
    {
        $manifest = [];
        foreach ($keys as $key) {
            $manifest[$key] = $key;
        }
        Config::$assetManifest = $manifest;
    }

    private function capture(callable $fn): string
    {
        ob_start();
        $fn();
        return (string) ob_get_clean();
    }
}