refactor: Divi-Boot-Schritte in DiviIntegration::boot() (Bootable) – Upgrader-Hook, Prüf-Route, CLI und Reiter „Divi“ über lit_mcp_admin_tabs; IntegrationRegistry::boot() bootet jede Integration genau einmal; Prüfseiten-Aufräumen im load-Hook statt beim Rendern

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 21:53:04 +02:00
co-authored by Claude Opus 5.5
parent 06148fb8c1
commit 6c59934caa
10 changed files with 293 additions and 37 deletions
+71 -3
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Admin;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use LIT\MCP\Admin\AdminPostAction;
use LIT\MCP\Admin\DiviTab;
@@ -14,10 +15,12 @@ use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Post;
final class DiviTabTest extends TestCase
{
@@ -41,6 +44,13 @@ final class DiviTabTest extends TestCase
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($key));
}
protected function tearDown(): void
{
unset($_GET['tab']);
parent::tearDown();
}
private static function report(bool $green = true, string $version = '5.3.0', string $key = 'key'): CheckReport
@@ -143,10 +153,9 @@ final class DiviTabTest extends TestCase
}
/** @return string The rendered tab. */
private function rendered(ReleaseStatus $status, ?CheckReport $report): string
private function rendered(ReleaseStatus $status, ?CheckReport $report, ?DiviTab $tab = null): string
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
Functions\when('get_posts')->justReturn([]);
Functions\when('rest_url')->justReturn('https://k.de/wp-json/lit-mcp/v1/divi-check');
Functions\when('wp_create_nonce')->justReturn('n');
Functions\when('wp_nonce_field')->justReturn('');
@@ -158,7 +167,7 @@ final class DiviTabTest extends TestCase
Functions\when('get_option')->justReturn('d.m.Y');
ob_start();
$this->tab($report, Mockery::mock(ReleaseStore::class))->render();
($tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)))->render();
return (string) ob_get_clean();
}
@@ -254,4 +263,63 @@ final class DiviTabTest extends TestCase
self::assertStringContainsString('Theme Builder: Nachbau von x() – y', $html);
self::assertStringContainsString('Plugin-Update', $html);
}
/** The tab removes the pages of a dead check in the settings page's load hook, never while rendering. */
public function testRegistersTheCleanupOnTheSettingsPageLoadHook(): void
{
Actions\expectAdded('admin_post_lit_mcp_divi_release')->once();
Actions\expectAdded('admin_post_lit_mcp_divi_withdraw')->once();
Actions\expectAdded('load-settings_page_lit-mcp')->once()->with(Mockery::type('array'));
$this->tab(null, Mockery::mock(ReleaseStore::class))->registerActions();
}
public function testTheLoadHookOnThisTabRemovesOrphansAndTheTabReportsThem(): void
{
$_GET['tab'] = 'divi';
Functions\expect('get_posts')->once()->andReturn([
new WP_Post(['ID' => 51, 'post_title' => WriteProbe::TITLE . ' 2026-10-18 08:00']),
]);
Functions\expect('wp_delete_post')->once()->with(51, true)->andReturn(new WP_Post(['ID' => 51]));
$tab = $this->tab(self::report(), Mockery::mock(ReleaseStore::class));
$tab->removeOrphans();
$html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report(), $tab);
self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html);
}
public function testRenderingAloneDeletesNothing(): void
{
Functions\expect('get_posts')->never();
Functions\expect('wp_delete_post')->never();
$html = $this->rendered(ReleaseStatus::released('5.3.0'), self::report());
self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html);
}
public function testTheLoadHookDeletesNothingOnOtherTabsWithoutTheRightOrWithoutDivi(): void
{
Functions\expect('get_posts')->never();
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
unset($_GET['tab']);
$tab->removeOrphans();
$_GET['tab'] = 'tools';
$tab->removeOrphans();
$_GET['tab'] = 'divi';
Functions\when('current_user_can')->justReturn(false);
$tab->removeOrphans();
Functions\when('current_user_can')->justReturn(true);
(new DiviTab(
Mockery::mock(CheckReportStore::class),
Mockery::mock(ReleaseStore::class),
FakeChangeLog::forbidden(),
null,
static fn (): string => ''
))->removeOrphans();
}
}
+18
View File
@@ -4,6 +4,7 @@ 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;
@@ -58,6 +59,23 @@ final class SettingsPageTest extends TestCase
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);