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:
co-authored by
Claude Opus 5.5
parent
06148fb8c1
commit
6c59934caa
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,14 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
|
||||
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Admin\DiviTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
|
||||
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
|
||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckRoute;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||||
use LIT\MCP\Integrations\WordPress\BuilderDetector;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
@@ -251,4 +258,36 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertSame(1, $asked);
|
||||
self::assertFalse((new DiviIntegration($schema, FakeChangeLog::forbidden()))->libraryIdsVisible());
|
||||
}
|
||||
|
||||
/** Bootable: Divi's own hooks – upgrader cache reset, check route, tab "Divi" after the plugin's tabs. */
|
||||
public function testBootRegistersDivisOwnHooks(): void
|
||||
{
|
||||
$upgraded = null;
|
||||
$tabs = null;
|
||||
Actions\expectAdded('upgrader_process_complete')->once()->whenHappen(
|
||||
static function (callable $callback) use (&$upgraded): void {
|
||||
$upgraded = $callback;
|
||||
}
|
||||
);
|
||||
Actions\expectAdded('rest_api_init')->once()->with(Mockery::on(
|
||||
static fn (array $callback): bool => $callback[0] instanceof DiviCheckRoute
|
||||
));
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once()->whenHappen(
|
||||
static function (callable $callback) use (&$tabs): void {
|
||||
$tabs = $callback;
|
||||
}
|
||||
);
|
||||
Functions\expect('delete_transient')->once()->with(ReleaseResolver::CACHE)->andReturn(true);
|
||||
$base = Mockery::mock(AdminTab::class);
|
||||
|
||||
(new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->boot();
|
||||
self::assertIsCallable($upgraded);
|
||||
$upgraded();
|
||||
self::assertIsCallable($tabs);
|
||||
$all = $tabs([$base]);
|
||||
|
||||
self::assertCount(2, $all);
|
||||
self::assertSame($base, $all[0]);
|
||||
self::assertInstanceOf(DiviTab::class, $all[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Integrations\Bootable;
|
||||
use LIT\MCP\Integrations\Core\CoreIntegration;
|
||||
use LIT\MCP\Integrations\Divi\DiviIntegration;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
@@ -79,6 +80,33 @@ final class IntegrationRegistryTest extends TestCase
|
||||
self::assertSame('Divi', (new DiviIntegration($schema, FakeChangeLog::forbidden()))->label());
|
||||
}
|
||||
|
||||
/** Integration-owned boot steps run once each, in registry order, whether or not the integration is available. */
|
||||
public function testBootsEveryBootableIntegrationOnce(): void
|
||||
{
|
||||
$booted = [];
|
||||
$bootable = function (string $id, bool $available) use (&$booted): Integration {
|
||||
$integration = Mockery::mock(Integration::class . ', ' . Bootable::class);
|
||||
$integration->allows('id')->andReturn($id);
|
||||
$integration->allows('isAvailable')->andReturn($available);
|
||||
$integration->expects('abilities')->never();
|
||||
$integration->expects('boot')->once()->andReturnUsing(static function () use (&$booted, $id): void {
|
||||
$booted[] = $id;
|
||||
});
|
||||
|
||||
return $integration;
|
||||
};
|
||||
$registry = new IntegrationRegistry(
|
||||
$this->integration('core', true, []),
|
||||
$bootable('divi', false),
|
||||
$bootable('pods', true)
|
||||
);
|
||||
|
||||
$registry->boot();
|
||||
$registry->boot();
|
||||
|
||||
self::assertSame(['divi', 'pods'], $booted);
|
||||
}
|
||||
|
||||
private function ability(string $name, bool $direct): Ability
|
||||
{
|
||||
$ability = Mockery::mock(Ability::class);
|
||||
|
||||
Reference in New Issue
Block a user