Die generische CheckRoute (POST lit-mcp/v1/<id>-check, für Divi weiter /divi-check) ersetzt DiviCheckRoute: Lock je Profil mit Besitzer-Token, Bericht je Profil, Rücksprung auf den neuen Reiter und Hook lit_mcp_compat_changed (Divi lädt darauf seinen Freigabe-Status neu). Der Reiter „Compatibility“ zeigt je Plugin Zustand (Wort und Icon), Hinweis zur Stufe, Fähigkeiten, getestete und freigegebene Versionen und den letzten Bericht, mit Prüfen, Freigeben und Zurückziehen (Recht manage_options, Allowlist der Profil-ID, Nonce je Profil). Freigeben serverseitig nur mit frischem grünem Bericht. Der Load-Hook lit_mcp_compat_profile_load bereitet den Abbau des DiviTab vor, der bis dahin parallel läuft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
12 KiB
PHP
312 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Admin;
|
|
|
|
use Brain\Monkey\Filters;
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Admin\AdminTab;
|
|
use LIT\MCP\Admin\SettingsPage;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
|
|
final class SettingsPageTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Functions\stubEscapeFunctions();
|
|
Functions\stubTranslationFunctions();
|
|
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://kunde.de/wp-admin/' . $path);
|
|
Functions\when('add_query_arg')->alias(static fn (array $args, string $url): string => $url . '?' . http_build_query($args));
|
|
Functions\when('sanitize_key')->alias(static fn (string $key): string => strtolower((string) preg_replace('/[^a-z0-9_\-]/i', '', $key)));
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
unset($_GET['tab'], $_GET['lit_mcp_notice'], $_GET['lit_mcp_profile']);
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function tab(string $slug, string $label): AdminTab
|
|
{
|
|
$tab = Mockery::mock(AdminTab::class);
|
|
$tab->allows('slug')->andReturn($slug);
|
|
$tab->allows('label')->andReturn($label);
|
|
$tab->allows('registerActions');
|
|
$tab->allows('render')->andReturnUsing(static function () use ($slug): void {
|
|
echo '<p>Inhalt ' . $slug . '</p>';
|
|
});
|
|
|
|
return $tab;
|
|
}
|
|
|
|
public function testRendersTabsAndTheRequestedTab(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['tab'] = 'zugriff';
|
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung'), $this->tab('zugriff', 'Zugriff')]);
|
|
|
|
ob_start();
|
|
$page->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('Inhalt zugriff', $html);
|
|
self::assertStringNotContainsString('Inhalt verbindung', $html);
|
|
self::assertMatchesRegularExpression('/tab=zugriff" class="nav-tab nav-tab-active" aria-current="page"/', $html);
|
|
self::assertMatchesRegularExpression('/tab=verbindung" class="nav-tab">/', $html);
|
|
self::assertSame(['verbindung', 'zugriff'], $page->tabSlugs());
|
|
}
|
|
|
|
/** Integrations add their tabs in Bootable::boot() (Divi: "Divi", last); the filter runs once, on first use. */
|
|
public function testIntegrationsAppendTheirTabsThroughTheFilter(): void
|
|
{
|
|
$divi = Mockery::mock(AdminTab::class);
|
|
$divi->allows('slug')->andReturn('divi');
|
|
$divi->expects('registerActions')->once();
|
|
Filters\expectApplied(SettingsPage::TABS_FILTER)->once()->andReturnUsing(
|
|
static fn (array $tabs): array => [...$tabs, $divi]
|
|
);
|
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung'), $this->tab('zugriff', 'Zugriff')]);
|
|
Functions\when('add_action')->justReturn(true);
|
|
|
|
$page->register();
|
|
|
|
self::assertSame(['verbindung', 'zugriff', 'divi'], $page->tabSlugs());
|
|
}
|
|
|
|
public function testRendersTheLogoTopRightOnlyWhenGiven(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
Functions\when('esc_url')->returnArg();
|
|
|
|
ob_start();
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')], 'https://example.test/logo.svg'))->render();
|
|
$withLogo = (string) ob_get_clean();
|
|
ob_start();
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->render();
|
|
$withoutLogo = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('<img src="https://example.test/logo.svg" alt="Loosen-IT Logo"', $withLogo);
|
|
self::assertStringNotContainsString('lit-mcp-logo', $withoutLogo);
|
|
// Logo sits in the header next to the h1 (flex, no absolute positioning over the title).
|
|
self::assertLessThan(strpos($withLogo, 'lit-mcp-logo'), strpos($withLogo, '<h1'));
|
|
self::assertStringNotContainsString('position:absolute', $withLogo);
|
|
}
|
|
|
|
public function testHeaderEndMarkerSitsBetweenTitleAndTabsWithNoticesAfterIt(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['lit_mcp_notice'] = 'revoked';
|
|
|
|
ob_start();
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
$h1 = (int) strpos($html, '<h1');
|
|
$end = strpos($html, '<hr class="wp-header-end">');
|
|
self::assertNotFalse($end);
|
|
self::assertLessThan($end, $h1);
|
|
self::assertLessThan(strpos($html, 'notice-success'), $end);
|
|
self::assertLessThan(strpos($html, 'nav-tab-wrapper'), (int) strpos($html, 'notice-success'));
|
|
self::assertStringContainsString('<nav class="nav-tab-wrapper wp-clearfix" aria-label="', $html);
|
|
}
|
|
|
|
public function testEnqueuesStylesAndCopyScriptOnlyOnItsOwnScreen(): void
|
|
{
|
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung')], '', 'https://example.test/assets', '1.2.3');
|
|
Functions\expect('wp_enqueue_style')->once()
|
|
->with('lit-mcp-admin', 'https://example.test/assets/css/admin.css', [], '1.2.3');
|
|
Functions\expect('wp_enqueue_script')->once()
|
|
->with('lit-mcp-admin', 'https://example.test/assets/js/admin.js', ['clipboard', 'wp-a11y'], '1.2.3', true);
|
|
Functions\when('wp_localize_script')->justReturn(true);
|
|
|
|
$page->enqueueAssets('settings_page_lit-mcp');
|
|
$page->enqueueAssets('options-general.php');
|
|
}
|
|
|
|
public function testToolsTabAlsoLoadsWordPressPostboxScriptForCollapsibleGroups(): void
|
|
{
|
|
$_GET['tab'] = 'tools';
|
|
$page = new SettingsPage([$this->tab('verbindung', 'V'), $this->tab('tools', 'T')], '', 'https://example.test/assets/', '1');
|
|
Functions\when('wp_enqueue_style')->justReturn(null);
|
|
Functions\expect('wp_enqueue_script')->once()
|
|
->with('lit-mcp-admin', 'https://example.test/assets/js/admin.js', ['clipboard', 'wp-a11y', 'postbox'], '1', true);
|
|
Functions\when('wp_localize_script')->justReturn(true);
|
|
|
|
$page->enqueueAssets('settings_page_lit-mcp');
|
|
}
|
|
|
|
public function testUnknownTabFallsBackToTheFirstAndNoticesAreShown(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['tab'] = '../../evil';
|
|
$_GET['lit_mcp_notice'] = 'revoked';
|
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung')]);
|
|
|
|
ob_start();
|
|
$page->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertSame('verbindung', $page->currentTab());
|
|
self::assertStringContainsString('Inhalt verbindung', $html);
|
|
self::assertStringContainsString('notice-success', $html);
|
|
}
|
|
|
|
public function testNothingIsRenderedWithoutManageOptions(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(false);
|
|
|
|
ob_start();
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->render();
|
|
|
|
self::assertSame('', ob_get_clean());
|
|
}
|
|
|
|
public function testRegisterHooksMenuSettingsAndTabActions(): void
|
|
{
|
|
$tab = Mockery::mock(AdminTab::class);
|
|
$tab->allows('slug')->andReturn('verbindung');
|
|
$tab->expects('registerActions')->once();
|
|
$page = new SettingsPage([$tab]);
|
|
Functions\expect('add_action')->once()->with('admin_menu', [$page, 'addMenu']);
|
|
Functions\expect('add_action')->once()->with('admin_init', [$page, 'registerSettings']);
|
|
Functions\expect('add_action')->once()->with('admin_enqueue_scripts', [$page, 'enqueueAssets']);
|
|
|
|
$page->register();
|
|
}
|
|
|
|
/** @dataProvider diviNotices */
|
|
public function testDiviNoticesAreShown(string $key, string $class, string $text): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['lit_mcp_notice'] = $key;
|
|
|
|
ob_start();
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('notice-' . $class, $html);
|
|
self::assertStringContainsString($text, $html);
|
|
}
|
|
|
|
/** @return list<list<string>> */
|
|
public static function diviNotices(): array
|
|
{
|
|
return [
|
|
['divi_released', 'success', 'für LIT MCP freigegeben'],
|
|
['divi_release_refused', 'error', 'Prüfung erneut ausführen'],
|
|
['divi_withdrawn', 'success', 'Freigabe zurückgezogen'],
|
|
['divi_withdraw_failed', 'error', 'keine Freigabe'],
|
|
];
|
|
}
|
|
|
|
/** @dataProvider compatNotices */
|
|
public function testCompatNoticesNameTheKnownProfile(string $key, string $class, string $text): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['lit_mcp_notice'] = $key;
|
|
$_GET['lit_mcp_profile'] = 'pods';
|
|
|
|
ob_start();
|
|
$this->pageWithProfiles()->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('notice-' . $class, $html);
|
|
self::assertStringContainsString('Pods <1>: ', $html);
|
|
self::assertStringContainsString($text, $html);
|
|
}
|
|
|
|
/** @return list<list<string>> */
|
|
public static function compatNotices(): array
|
|
{
|
|
return [
|
|
['compat_checked', 'success', 'Prüfung abgeschlossen'],
|
|
['compat_check_running', 'warning', 'bereits eine Prüfung'],
|
|
['compat_check_failed', 'error', 'PHP-Error-Log'],
|
|
['compat_released', 'success', 'freigegeben'],
|
|
['compat_release_refused', 'error', 'Prüfung erneut ausführen'],
|
|
['compat_withdrawn', 'success', 'Freigabe zurückgezogen'],
|
|
['compat_withdraw_failed', 'error', 'keine Freigabe'],
|
|
];
|
|
}
|
|
|
|
public function testAnUnknownProfileInTheNoticeIsIgnored(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['lit_mcp_notice'] = 'compat_checked';
|
|
$_GET['lit_mcp_profile'] = '<script>x</script>';
|
|
|
|
ob_start();
|
|
$this->pageWithProfiles()->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringContainsString('Prüfung abgeschlossen', $html);
|
|
self::assertStringNotContainsString('script', $html);
|
|
self::assertStringNotContainsString('Pods', $html);
|
|
}
|
|
|
|
/** The checks of all profiles end in compat_* notices; the old Divi-only check notices are gone. */
|
|
public function testTheRetiredDiviCheckNoticesShowNothing(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
|
|
foreach (['divi_checked', 'divi_check_running', 'divi_check_failed'] as $key) {
|
|
$_GET['lit_mcp_notice'] = $key;
|
|
ob_start();
|
|
$this->pageWithProfiles()->render();
|
|
|
|
self::assertStringNotContainsString('class="notice', (string) ob_get_clean(), $key);
|
|
}
|
|
}
|
|
|
|
public function testArrayValuesInTheQueryAreIgnoredWithoutAWarning(): void
|
|
{
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$_GET['tab'] = ['x'];
|
|
$_GET['lit_mcp_notice'] = ['compat_checked'];
|
|
$_GET['lit_mcp_profile'] = ['pods'];
|
|
|
|
ob_start();
|
|
$this->pageWithProfiles()->render();
|
|
$html = (string) ob_get_clean();
|
|
|
|
self::assertStringNotContainsString('class="notice', $html);
|
|
$_GET['lit_mcp_notice'] = 'compat_checked';
|
|
ob_start();
|
|
$this->pageWithProfiles()->render();
|
|
self::assertStringNotContainsString('Pods', (string) ob_get_clean());
|
|
}
|
|
|
|
private function pageWithProfiles(): SettingsPage
|
|
{
|
|
return new SettingsPage(
|
|
[$this->tab('verbindung', 'Verbindung')],
|
|
profileLabel: static fn (string $id): ?string => $id === 'pods' ? 'Pods <1>' : null
|
|
);
|
|
}
|
|
|
|
public function testTheNoticeParametersAreRemovedFromTheAddressBarAfterwards(): void
|
|
{
|
|
$page = new SettingsPage([$this->tab('verbindung', 'Verbindung')]);
|
|
|
|
self::assertSame(['a', 'lit_mcp_notice', 'lit_mcp_profile'], $page->removableQueryArgs(['a']));
|
|
}
|
|
|
|
public function testAddsAnOverviewHelpTab(): void
|
|
{
|
|
Functions\stubEscapeFunctions();
|
|
Functions\stubTranslationFunctions();
|
|
$screen = Mockery::mock();
|
|
$screen->expects('add_help_tab')->once()->with(Mockery::on(
|
|
static fn (array $tab): bool => $tab['id'] === 'lit-mcp-overview'
|
|
&& str_contains($tab['content'], 'Tab „Verbindung“')
|
|
&& str_contains($tab['content'], 'Tab „Tools“')
|
|
&& str_contains($tab['content'], 'Tab „Compatibility“')
|
|
));
|
|
|
|
(new SettingsPage([$this->tab('verbindung', 'Verbindung')]))->helpTabFor($screen);
|
|
}
|
|
}
|