feat: lit-site-status, Admin-Tab und Admin-Hinweis zeigen Divi-Fähigkeiten mit Zustand, Quelle, Problemen und betroffenen Werkzeugen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 22:29:38 +02:00
co-authored by Claude Sonnet 5.5
parent b92545a8fe
commit 2aef6e1154
13 changed files with 831 additions and 31 deletions
+214 -4
View File
@@ -11,6 +11,9 @@ use LIT\MCP\Admin\DiviTab;
use LIT\MCP\Integrations\Divi\Release\CheckReport;
use LIT\MCP\Integrations\Divi\Release\CheckReportStore;
use LIT\MCP\Integrations\Divi\Release\CheckStep;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
@@ -18,6 +21,8 @@ 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\Support\FeatureFakeTool;
use LIT\MCP\Tests\Support\MatrixFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Post;
@@ -63,8 +68,12 @@ final class DiviTabTest extends TestCase
return new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, $version, Plugin::VERSION, $key, $steps);
}
private function tab(?CheckReport $report, ReleaseStore $releases, ?FakeChangeLog $log = null): DiviTab
{
private function tab(
?CheckReport $report,
ReleaseStore $releases,
?FakeChangeLog $log = null,
?FeatureOverview $overview = null
): DiviTab {
$reports = Mockery::mock(CheckReportStore::class);
$reports->allows('latest')->andReturn($report);
@@ -75,7 +84,8 @@ final class DiviTabTest extends TestCase
new AdminPostAction(static function (): void {
}),
static fn (): string => '5.3.0',
static fn (string $version): string => 'key'
static fn (string $version): string => 'key',
$overview
);
}
@@ -154,6 +164,19 @@ final class DiviTabTest extends TestCase
/** @return string The rendered tab. */
private function rendered(ReleaseStatus $status, ?CheckReport $report, ?DiviTab $tab = null): string
{
return $this->renderedTab($status, $tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)));
}
private function renderedWith(ReleaseStatus $status, ?CheckReport $report, FeatureOverview $overview): string
{
return $this->renderedTab(
$status,
$this->tab($report, Mockery::mock(ReleaseStore::class), null, $overview)
);
}
private function renderedTab(ReleaseStatus $status, DiviTab $tab): string
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
Functions\when('rest_url')->justReturn('https://k.de/wp-json/lit-mcp/v1/divi-check');
@@ -167,7 +190,7 @@ final class DiviTabTest extends TestCase
Functions\when('get_option')->justReturn('d.m.Y');
ob_start();
($tab ?? $this->tab($report, Mockery::mock(ReleaseStore::class)))->render();
$tab->render();
return (string) ob_get_clean();
}
@@ -331,4 +354,191 @@ final class DiviTabTest extends TestCase
static fn (): string => ''
))->removeOrphans();
}
private static function mixedStatus(): ReleaseStatus
{
$matrix = MatrixFixtures::released('auto', [
new FeatureStatus(
DiviFeature::PageWrite,
ReleaseState::Incompatible,
['Seiten: Signatur von et_pb_x() <geändert>'],
null
),
new FeatureStatus(DiviFeature::Catalog, ReleaseState::Untested, [], null),
]);
return new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, [], null, matrix: $matrix);
}
public function testTheFeatureTableShowsStateSourceProblemsAndTools(): void
{
$overview = new FeatureOverview(static fn (): array => [
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
new FeatureFakeTool('lit/divi-presets-assign', false, [DiviFeature::PresetWrite]),
]);
$html = $this->renderedWith(self::mixedStatus(), null, $overview);
self::assertStringContainsString('<th scope="col">Betroffene Werkzeuge</th>', $html);
self::assertStringContainsString(
'<td>Seiten schreiben</td><td><strong>inkompatibel</strong></td><td>–</td>',
$html
);
self::assertStringContainsString('<td>Modulkatalog</td><td><strong>ungetestet</strong></td>', $html);
self::assertStringContainsString(
'<td>Seiten lesen</td><td><strong>freigegeben</strong></td><td>Automatik (auto)</td>',
$html
);
self::assertStringContainsString('<li><code>lit-divi-insert</code></li>', $html);
self::assertStringContainsString('<li><code>lit/divi-presets-assign</code></li>', $html);
}
public function testFeatureProblemsAreEscaped(): void
{
$html = $this->rendered(self::mixedStatus(), null);
self::assertStringContainsString('Seiten: Signatur von et_pb_x() &lt;geändert&gt;', $html);
self::assertStringNotContainsString('<geändert>', $html);
}
public function testTheTableNamesEverySourceInWords(): void
{
$code = $this->rendered(ReleaseStatus::released('5.3.0'), null);
$database = $this->rendered(
new ReleaseStatus(ReleaseState::Released, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
], matrix: MatrixFixtures::released('database')),
self::report()
);
self::assertStringContainsString('Plugin (code)', $code);
self::assertStringContainsString('Freigabe auf dieser Website (database)', $database);
}
public function testTheTabExplainsTheAutomationAndTheFilter(): void
{
$html = $this->rendered(ReleaseStatus::released('5.3.0'), null);
self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html);
self::assertStringContainsString('<code>lit_mcp_divi_auto_release</code>', $html);
self::assertStringContainsString('als ungetestet', $html);
self::assertStringContainsString('Aufrufe', $html);
self::assertStringNotContainsString('Verhalten geprüft', $html);
}
public function testTheTabNoLongerSaysWritingIsLockedAsAWhole(): void
{
$untested = $this->rendered(
new ReleaseStatus(
ReleaseState::Untested,
'5.3.0',
matrix: MatrixFixtures::with(DiviFeature::Catalog, ReleaseState::Untested)
),
null
);
$incompatible = $this->rendered(self::mixedStatus(), null);
foreach ([$untested, $incompatible] as $html) {
self::assertStringNotContainsString('Schreiben bleibt gesperrt', $html);
self::assertStringNotContainsString('lesen nur', $html);
self::assertStringContainsString('alle anderen laufen weiter', $html);
}
}
public function testADatabaseReleaseCanBeWithdrawnWhileOneFeatureIsIncompatible(): void
{
$matrix = MatrixFixtures::released('database', [
new FeatureStatus(DiviFeature::PageWrite, ReleaseState::Incompatible, ['Seiten: x() fehlt'], null),
]);
$status = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', ReleaseStatus::SOURCE_DATABASE, [], [
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
], matrix: $matrix);
self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report()));
}
public function testWithoutADatabaseReleaseNothingCanBeWithdrawnWhateverTheState(): void
{
self::assertStringNotContainsString('Freigabe zurückziehen', $this->rendered(self::mixedStatus(), null));
}
/** @return string The admin notice of this request. */
private function notice(ReleaseStatus $status, string $version = '5.3.0'): string
{
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
$tab = new DiviTab(
Mockery::mock(CheckReportStore::class),
Mockery::mock(ReleaseStore::class),
FakeChangeLog::forbidden(),
new AdminPostAction(static function (): void {
}),
static fn () => $version,
static fn (string $v): string => 'key'
);
ob_start();
$tab->renderNotice();
return (string) ob_get_clean();
}
public function testTheNoticeNamesTheUnusableFeaturesAndLinksToTheTab(): void
{
$html = $this->notice(self::mixedStatus());
self::assertStringContainsString('class="notice notice-warning"', $html);
self::assertStringContainsString('Mit Divi 5.3.0 sind diese Fähigkeiten nicht nutzbar', $html);
self::assertStringContainsString('Seiten schreiben, Modulkatalog', $html);
self::assertStringContainsString('alle anderen laufen weiter', $html);
self::assertStringContainsString('page=lit-mcp&tab=divi', $html);
}
public function testNoNoticeWhileEveryFeatureIsUsable(): void
{
self::assertSame('', $this->notice(ReleaseStatus::released('5.3.0')));
}
public function testNoNoticeForUsersWithoutManageOptions(): void
{
Functions\when('current_user_can')->justReturn(false);
self::assertSame('', $this->notice(self::mixedStatus()));
}
public function testNoNoticeWithoutDivi(): void
{
self::assertSame('', $this->notice(self::mixedStatus(), ''));
}
public function testNoNoticeOnTheDiviTabItself(): void
{
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
$_GET['tab'] = 'divi';
self::assertSame('', $this->notice(self::mixedStatus()));
}
public function testTheNoticeStillShowsOnOtherTabsOfThePluginPage(): void
{
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
$_GET['tab'] = 'tools';
self::assertStringContainsString('nicht nutzbar', $this->notice(self::mixedStatus()));
}
public function testTheNoticeIsRegisteredOnAdminNotices(): void
{
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
$tab->registerNotice();
}
public function testRegisteringTheTabsActionsAlsoRegistersTheNotice(): void
{
$tab = $this->tab(null, Mockery::mock(ReleaseStore::class));
Actions\expectAdded('admin_notices')->once()->with([$tab, 'renderNotice']);
$tab->registerActions();
}
}
@@ -33,4 +33,28 @@ final class SiteStatusAbilityTest extends TestCase
self::assertSame(['administrator', 'editor'], $result['allowed_roles']);
self::assertSame('divi', $result['components'][0]['id']);
}
public function testTheOutputSchemaDescribesTheDiviFeatures(): void
{
$schema = (new SiteStatusAbility(Mockery::mock(EnvironmentProbe::class), new Access()))->outputSchema();
$component = $schema['properties']['components']['items'];
$feature = $component['properties']['features']['items'];
self::assertSame(['id', 'active', 'version', 'problems', 'release'], $component['required']);
self::assertSame(
['id', 'label', 'state', 'source', 'problems', 'tools'],
$feature['required']
);
self::assertSame(['released', 'untested', 'incompatible'], $feature['properties']['state']['enum']);
self::assertSame(['string', 'null'], $feature['properties']['source']['type']);
}
public function testTheDescriptionNamesTheFeaturesAndSaysOtherToolsKeepWorking(): void
{
$description = (new SiteStatusAbility(Mockery::mock(EnvironmentProbe::class), new Access()))->description();
self::assertStringContainsString('features je Fähigkeit', $description);
self::assertStringContainsString('(code, database, auto)', $description);
self::assertStringContainsString('alle anderen Divi-Werkzeuge laufen weiter', $description);
}
}
@@ -8,6 +8,13 @@ use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Core\EnvironmentComponent;
use LIT\MCP\Integrations\Divi\DiviCompatibility;
use LIT\MCP\Integrations\Divi\DiviEnvironmentComponent;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Tests\Support\FeatureFakeTool;
use LIT\MCP\Tests\Support\MatrixFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -61,8 +68,78 @@ final class DiviEnvironmentComponentTest extends TestCase
))->describe();
self::assertSame(
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null],
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null,
'features' => []],
$described
);
}
public function testListsEveryFeatureWithStateSourceProblemsAndTools(): void
{
$this->theme('Divi', '5.3.0');
$matrix = MatrixFixtures::with(
DiviFeature::PageWrite,
ReleaseState::Incompatible,
'auto',
'Seiten: Signatur von et_pb_x() geändert'
);
ReleaseGate::useResolver(static fn (): ReleaseStatus => new ReleaseStatus(
ReleaseState::Incompatible,
'5.3.0',
null,
[],
null,
matrix: $matrix
));
$overview = new FeatureOverview(static fn (): array => [
new FeatureFakeTool('lit/divi-outline', true, [DiviFeature::PageRead]),
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
]);
$component = (new DiviEnvironmentComponent(new DiviCompatibility([]), $overview))->describe();
$features = array_column($component['features'], null, 'id');
self::assertSame('divi', $component['id']);
self::assertCount(count(DiviFeature::cases()), $features);
self::assertSame('incompatible', $features['page-write']['state']);
self::assertNull($features['page-write']['source']);
self::assertSame(['Seiten: Signatur von et_pb_x() geändert'], $features['page-write']['problems']);
self::assertSame(['lit-divi-insert'], $features['page-write']['tools']);
self::assertSame('Seiten schreiben', $features['page-write']['label']);
self::assertSame('released', $features['page-read']['state']);
self::assertSame('auto', $features['page-read']['source']);
self::assertSame(['lit-divi-outline', 'lit-divi-insert'], $features['page-read']['tools']);
self::assertSame('incompatible', $component['release']['state']);
}
public function testTheMatrixComesFromTheGateAndIsNotComputedAgain(): void
{
$this->theme('Divi', '5.3.0');
$calls = 0;
ReleaseGate::useResolver(static function () use (&$calls): ReleaseStatus {
++$calls;
return ReleaseStatus::released('5.3.0');
});
$component = new DiviEnvironmentComponent(new DiviCompatibility([]));
$component->describe();
$component->describe();
self::assertSame(1, $calls);
}
public function testWithoutDiviThereAreNoFeaturesAndNoGateCall(): void
{
$this->theme('twentytwentyfive', '1.0');
ReleaseGate::useResolver(static function (): ReleaseStatus {
self::fail('the gate must not be asked without Divi');
});
$component = (new DiviEnvironmentComponent(new DiviCompatibility([])))->describe();
self::assertFalse($component['active']);
self::assertSame([], $component['features']);
self::assertNull($component['release']);
}
}
@@ -20,6 +20,7 @@ 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\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Integration;
use LIT\MCP\Integrations\IntegrationRegistry;
@@ -27,6 +28,7 @@ use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Integrations\WordPress\BuilderDetector;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Support\FeatureFakeTool;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
@@ -327,4 +329,28 @@ final class DiviIntegrationTest extends TestCase
self::assertSame($base, $all[0]);
self::assertInstanceOf(DiviTab::class, $all[1]);
}
public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void
{
$integration = new DiviIntegration(
Mockery::mock(ContentSchema::class),
FakeChangeLog::forbidden(),
allAbilities: static fn (): array => [
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageWrite]),
new FeatureFakeTool('lit/library-insert', true, [DiviFeature::PageWrite, DiviFeature::LibraryRead]),
]
);
self::assertSame(
['page-write' => ['lit-divi-insert', 'lit-library-insert'], 'library-read' => ['lit-library-insert']],
$integration->featureOverview()->toolsByFeature()
);
}
public function testWithoutAnAbilityListTheOverviewKnowsNoTools(): void
{
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
self::assertSame([], $integration->featureOverview()->toolsByFeature());
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Tests\Support\FeatureFakeTool;
use LIT\MCP\Tests\Support\MatrixFixtures;
use PHPUnit\Framework\TestCase;
final class FeatureOverviewTest extends TestCase
{
private static function overview(): FeatureOverview
{
return new FeatureOverview(static fn (): array => [
new FeatureFakeTool('lit/divi-outline', true, [DiviFeature::PageRead]),
new FeatureFakeTool('lit/divi-insert', true, [DiviFeature::PageRead, DiviFeature::PageWrite]),
new FeatureFakeTool('lit/divi-presets-assign', false, [DiviFeature::PresetWrite]),
new FeatureFakeTool('lit/content-create', true, []),
]);
}
public function testToolsAreNamedAsTheClientSeesThem(): void
{
$tools = self::overview()->toolsByFeature();
self::assertSame(['lit-divi-outline', 'lit-divi-insert'], $tools['page-read']);
self::assertSame(['lit-divi-insert'], $tools['page-write']);
self::assertSame(['lit/divi-presets-assign'], $tools['preset-write']);
}
public function testAToolWithoutFeaturesIsLeftOut(): void
{
$overview = new FeatureOverview(static fn (): array => [new FeatureFakeTool('lit/content-create', true, [])]);
self::assertSame([], $overview->toolsByFeature());
}
public function testRowsCarryTheMatrixAndTheToolsOfEachFeature(): void
{
$matrix = MatrixFixtures::with(
DiviFeature::PageWrite,
ReleaseState::Incompatible,
'code',
'Seiten: et_pb_x() fehlt'
);
$rows = array_column(self::overview()->rows($matrix), null, 'id');
self::assertCount(count(DiviFeature::cases()), $rows);
self::assertSame(
[
'id' => 'page-write',
'label' => 'Seiten schreiben',
'state' => 'incompatible',
'problems' => ['Seiten: et_pb_x() fehlt'],
'source' => null,
'tools' => ['lit-divi-insert'],
],
$rows['page-write']
);
self::assertSame('released', $rows['page-read']['state']);
self::assertSame('code', $rows['page-read']['source']);
self::assertSame([], $rows['catalog']['tools']);
}
public function testWithoutToolsEveryRowHasAnEmptyToolList(): void
{
foreach (FeatureOverview::withoutTools()->rows(MatrixFixtures::released()) as $row) {
self::assertSame([], $row['tools']);
}
}
}