refactor(divi): DiviTab geht im Reiter „Compatibility“ auf
Der eigene Divi-Reiter entfällt. DiviAdminExtras behält Admin-Hinweis, Orphan-Bereinigung vor der Ausgabe (lit_mcp_compat_profile_load), Hinweis zum nächsten Schritt und zur privaten Testseite (lit_mcp_compat_profile_intro), Zustandstexte mit Quelle (lit_mcp_compat_explanation), Automatik-Notiz und Visual-Builder-Hinweis (lit_mcp_compat_profile_extra) sowie die Spalten Quelle, Probleme und Betroffene Tools über die neue, gegen fehlerhafte Callbacks abgesicherte Erweiterung lit_mcp_compat_capability_columns. Texte, Doku, CSS (lit-mcp-compat-*) und Smoke auf den neuen Reiter umgestellt, tote divi_*-Hinweise entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
85f4138b36
commit
b40b81f585
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Admin;
|
||||
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\AdminPostAction;
|
||||
@@ -312,6 +313,45 @@ final class CompatibilityTabTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testAProfileCanAddColumnsToTheCapabilityTableAndTheTabEscapesThem(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->with([], 'fake')->andReturn([
|
||||
[
|
||||
'heading' => 'Quelle <b>',
|
||||
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => $c === FakeCapability::Read
|
||||
? ['<i>a</i>']
|
||||
: [],
|
||||
],
|
||||
[
|
||||
'heading' => 'Tools',
|
||||
'cell' => static fn (FakeCapability $c, CompatibilityMatrix $m): array => ['tool-' . $c->id()],
|
||||
'code' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$html = self::html($this->tab(self::profile()));
|
||||
|
||||
self::assertStringContainsString('<th scope="col">Quelle <b></th>', $html);
|
||||
self::assertStringContainsString('<td data-label="Quelle <b>"><ul><li><i>a</i></li></ul></td>', $html);
|
||||
self::assertStringContainsString('<td data-label="Quelle <b>">–</td>', $html);
|
||||
self::assertStringContainsString('<li><code>tool-' . FakeCapability::Read->id() . '</code></li>', $html);
|
||||
self::assertStringNotContainsString('<i>a</i>', $html);
|
||||
}
|
||||
|
||||
public function testTheCapabilityTableIsOpenUnlessReleasedAndFolded(): void
|
||||
{
|
||||
$released = self::html($this->tab(
|
||||
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Released))
|
||||
));
|
||||
$untested = self::html($this->tab(
|
||||
self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested))
|
||||
));
|
||||
|
||||
self::assertStringContainsString('<details class="lit-mcp-compat-details">', $released);
|
||||
self::assertStringContainsString('<details class="lit-mcp-compat-details" open>', $untested);
|
||||
self::assertStringContainsString('lit-mcp-compat-features', $released);
|
||||
}
|
||||
|
||||
public function testEveryButtonNamesItsProfileForAssistiveTechnology(): void
|
||||
{
|
||||
$profile = self::profile(Tier::ReleasePerVersion, self::matrix(ReleaseState::Untested));
|
||||
@@ -430,6 +470,120 @@ final class CompatibilityTabTest extends TestCase
|
||||
self::html($this->tab(self::profile(), self::report()));
|
||||
}
|
||||
|
||||
public function testTheProfileIntroRunsBeforeTheCapabilityTableAndTheExtraAfterIt(): void
|
||||
{
|
||||
Actions\expectDone(CompatibilityTab::INTRO_ACTION)->once()->with('fake')->whenHappen(static function (): void {
|
||||
echo '[INTRO]';
|
||||
});
|
||||
Actions\expectDone(CompatibilityTab::EXTRA_ACTION)->once()->with('fake')->whenHappen(static function (): void {
|
||||
echo '[EXTRA]';
|
||||
});
|
||||
|
||||
$html = self::html($this->tab(self::profile(), self::report()));
|
||||
|
||||
self::assertLessThan(strpos($html, 'lit-mcp-compat-details'), strpos($html, '[INTRO]'));
|
||||
self::assertGreaterThan(strpos($html, '</table></details>'), strpos($html, '[EXTRA]'));
|
||||
}
|
||||
|
||||
public function testAnInactiveProfileFiresNeitherIntroNorExtra(): void
|
||||
{
|
||||
Actions\expectDone(CompatibilityTab::INTRO_ACTION)->never();
|
||||
Actions\expectDone(CompatibilityTab::EXTRA_ACTION)->never();
|
||||
|
||||
self::html($this->tab(self::profile(active: false)));
|
||||
}
|
||||
|
||||
public function testAProfileCanReplaceTheGenericExplanationAndTheTabEscapesIt(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::EXPLANATION_FILTER)
|
||||
->once()
|
||||
->with('Alle Fähigkeiten sind nutzbar.', 'fake', ReleaseState::Released)
|
||||
->andReturn('Eigener <b>Text</b>');
|
||||
|
||||
$html = self::html($this->tab(self::profile()));
|
||||
|
||||
self::assertStringContainsString('Eigener <b>Text</b>', $html);
|
||||
self::assertStringNotContainsString('Alle Fähigkeiten sind nutzbar.', $html);
|
||||
}
|
||||
|
||||
public function testANonStringExplanationFromTheFilterKeepsTheGenericText(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::EXPLANATION_FILTER)->once()->andReturn(['x']);
|
||||
|
||||
self::assertStringContainsString('Alle Fähigkeiten sind nutzbar.', self::html($this->tab(self::profile())));
|
||||
}
|
||||
|
||||
/** @return array{heading: string, cell: Closure} */
|
||||
private static function column(string $heading, Closure $cell): array
|
||||
{
|
||||
return ['heading' => $heading, 'cell' => $cell];
|
||||
}
|
||||
|
||||
public function testInvalidColumnsAreDroppedAndTheRestIsReindexed(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||
3 => self::column('Gut', static fn (): array => ['ok']),
|
||||
5 => ['heading' => 42, 'cell' => static fn (): array => ['x']],
|
||||
6 => ['heading' => 'Ohne Closure', 'cell' => 'strlen'],
|
||||
7 => 'kaputt',
|
||||
9 => ['heading' => 'Ohne Zelle'],
|
||||
]);
|
||||
|
||||
$html = self::html($this->tab(self::profile()));
|
||||
|
||||
self::assertStringContainsString('<th scope="col">Gut</th>', $html);
|
||||
self::assertSame(3, substr_count($html, '<th scope="col">'));
|
||||
self::assertStringContainsString('<td data-label="Gut"><ul><li>ok</li></ul></td>', $html);
|
||||
}
|
||||
|
||||
public function testAThrowingCellShowsADashLogsTheFailureAndKeepsTheTableClosed(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||
self::column('Kaputt', static function (): array {
|
||||
throw new \RuntimeException('Zelle <kaputt>');
|
||||
}),
|
||||
self::column('Heil', static fn (): array => ['ok']),
|
||||
]);
|
||||
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||
$previous = ini_set('error_log', $log);
|
||||
|
||||
try {
|
||||
$html = self::html($this->tab(self::profile()));
|
||||
} finally {
|
||||
ini_set('error_log', (string) $previous);
|
||||
}
|
||||
$logged = (string) file_get_contents($log);
|
||||
unlink($log);
|
||||
|
||||
self::assertStringContainsString('<td data-label="Kaputt">–</td>', $html);
|
||||
self::assertStringContainsString('<td data-label="Heil"><ul><li>ok</li></ul></td>', $html);
|
||||
self::assertStringContainsString('</tbody></table></details>', $html);
|
||||
self::assertStringContainsString('fake capability column "Kaputt" failed', $logged);
|
||||
self::assertStringContainsString('Zelle <kaputt>', $logged);
|
||||
}
|
||||
|
||||
public function testACellThatReturnsNoListShowsADash(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn([
|
||||
self::column('Text', static fn (): string => 'kein Array'),
|
||||
self::column('Karte', static fn (): array => ['a' => 'x']),
|
||||
self::column('Zahlen', static fn (): array => [1, 2]),
|
||||
]);
|
||||
|
||||
$html = self::html($this->tab(self::profile()));
|
||||
|
||||
self::assertStringContainsString('<td data-label="Text">–</td>', $html);
|
||||
self::assertStringContainsString('<td data-label="Karte">–</td>', $html);
|
||||
self::assertStringContainsString('<td data-label="Zahlen">–</td>', $html);
|
||||
}
|
||||
|
||||
public function testANonArrayColumnsFilterResultMeansNoExtraColumns(): void
|
||||
{
|
||||
Filters\expectApplied(CompatibilityTab::COLUMNS_FILTER)->once()->andReturn('kaputt');
|
||||
|
||||
self::assertSame(2, substr_count(self::html($this->tab(self::profile())), '<th scope="col">'));
|
||||
}
|
||||
|
||||
public function testEverythingFromThirdPartiesIsEscaped(): void
|
||||
{
|
||||
$profile = self::profile(
|
||||
|
||||
@@ -177,31 +177,6 @@ final class SettingsPageTest extends TestCase
|
||||
$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
|
||||
{
|
||||
|
||||
@@ -154,8 +154,8 @@ final class CoreIndependenceTest extends TestCase
|
||||
|
||||
self::assertArrayHasKey('Plugin.php', $files, 'root files are scanned');
|
||||
self::assertArrayHasKey('Integrations/IntegrationRegistry.php', $files, 'Integrations root files are scanned');
|
||||
self::assertArrayNotHasKey('Admin/DiviTab.php', $files, 'the Divi tab belongs to Integrations/Divi/Admin');
|
||||
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviTab.php');
|
||||
self::assertArrayNotHasKey('Admin/DiviAdminExtras.php', $files, 'the Divi additions belong to Integrations/Divi/Admin');
|
||||
self::assertFileExists(self::root() . 'Integrations/Divi/Admin/DiviAdminExtras.php');
|
||||
self::assertFileExists(self::root() . 'Integrations/Divi/Cli/DiviCheckCommand.php');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Admin;
|
||||
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviAdminExtras;
|
||||
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\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\WriteProbe;
|
||||
use LIT\MCP\Tests\Support\FeatureFakeTool;
|
||||
use LIT\MCP\Tests\Support\MatrixFixtures;
|
||||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
|
||||
final class DiviAdminExtrasTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.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($key));
|
||||
Functions\when('wp_unslash')->returnArg();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_GET['tab']);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private static function extras(
|
||||
string $version = '5.3.0',
|
||||
?FeatureOverview $overview = null,
|
||||
bool $report = false
|
||||
): DiviAdminExtras {
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->allows('latest')->andReturn($report ? new CheckReport('r1', '2026-10-01T08:00:00+00:00', 1, '5.3.0', '0.7.0', 'key', []) : null);
|
||||
|
||||
return new DiviAdminExtras($overview, static fn (): string => $version, $reports);
|
||||
}
|
||||
|
||||
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 ReleaseStatus::of('5.3.0', $matrix);
|
||||
}
|
||||
|
||||
private static function extraHtml(ReleaseStatus $status, ?DiviAdminExtras $extras = null, string $id = 'divi'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
|
||||
ob_start();
|
||||
($extras ?? self::extras())->renderExtras($id);
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
private static function introHtml(ReleaseStatus $status, ?DiviAdminExtras $extras = null, string $id = 'divi'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
|
||||
ob_start();
|
||||
($extras ?? self::extras())->renderIntro($id);
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
private static function explanationOf(ReleaseStatus $status, string $id = 'divi', string $generic = 'generisch'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
|
||||
return self::extras()->explanation($generic, $id, $status->state);
|
||||
}
|
||||
|
||||
private static function noticeHtml(ReleaseStatus $status, string $version = '5.3.0'): string
|
||||
{
|
||||
ReleaseGate::useResolver(static fn (): ReleaseStatus => $status);
|
||||
|
||||
ob_start();
|
||||
self::extras($version)->renderNotice();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testItHooksIntoTheCompatibilityTabAndTheAdminNotices(): void
|
||||
{
|
||||
$extras = self::extras();
|
||||
Actions\expectAdded('admin_notices')->once()->with([$extras, 'renderNotice']);
|
||||
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once()->with([$extras, 'removeOrphans']);
|
||||
Actions\expectAdded(CompatibilityTab::INTRO_ACTION)->once()->with([$extras, 'renderIntro']);
|
||||
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once()->with([$extras, 'renderExtras']);
|
||||
Filters\expectAdded(CompatibilityTab::EXPLANATION_FILTER)->once()->with([$extras, 'explanation'], 10, 3);
|
||||
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once()->with([$extras, 'columns'], 10, 2);
|
||||
|
||||
$extras->register();
|
||||
}
|
||||
|
||||
public function testTheNoticeNamesTheUnusableFeaturesAndLinksToTheCompatibilityTab(): void
|
||||
{
|
||||
$html = self::noticeHtml(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=compatibility', $html);
|
||||
self::assertStringContainsString('Reiter „Compatibility“', $html);
|
||||
}
|
||||
|
||||
public function testNoNoticeWhileEveryFeatureIsUsable(): void
|
||||
{
|
||||
self::assertSame('', self::noticeHtml(ReleaseMatrices::released('5.3.0')));
|
||||
}
|
||||
|
||||
public function testNoNoticeForUsersWithoutManageOptions(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertSame('', self::noticeHtml(self::mixedStatus()));
|
||||
}
|
||||
|
||||
public function testNoNoticeWithoutDivi(): void
|
||||
{
|
||||
self::assertSame('', self::noticeHtml(self::mixedStatus(), ''));
|
||||
}
|
||||
|
||||
public function testNoNoticeOnTheCompatibilityTabItself(): void
|
||||
{
|
||||
Functions\when('get_current_screen')->justReturn((object) ['id' => 'settings_page_lit-mcp']);
|
||||
$_GET['tab'] = 'compatibility';
|
||||
|
||||
self::assertSame('', self::noticeHtml(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', self::noticeHtml(self::mixedStatus()));
|
||||
}
|
||||
|
||||
/** The orphans go in the load action of the tab, never while rendering. */
|
||||
public function testTheLoadActionRemovesOrphansAndTheExtrasReportThem(): void
|
||||
{
|
||||
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]));
|
||||
$extras = self::extras();
|
||||
|
||||
$extras->removeOrphans('divi');
|
||||
$html = self::introHtml(ReleaseMatrices::released('5.3.0'), $extras);
|
||||
|
||||
self::assertStringContainsString('Liegengebliebene Prüfseiten gelöscht: 51', $html);
|
||||
self::assertStringNotContainsString('Liegengebliebene', self::extraHtml(ReleaseMatrices::released('5.3.0'), $extras));
|
||||
}
|
||||
|
||||
public function testRenderingAloneDeletesNothing(): void
|
||||
{
|
||||
Functions\expect('get_posts')->never();
|
||||
Functions\expect('wp_delete_post')->never();
|
||||
|
||||
$html = self::introHtml(ReleaseMatrices::released('5.3.0'));
|
||||
|
||||
self::assertStringNotContainsString('Liegengebliebene Prüfseiten', $html);
|
||||
}
|
||||
|
||||
public function testTheLoadActionDeletesNothingForOtherProfilesWithoutTheRightOrWithoutDivi(): void
|
||||
{
|
||||
Functions\expect('get_posts')->never();
|
||||
|
||||
self::extras()->removeOrphans('pods');
|
||||
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
self::extras()->removeOrphans('divi');
|
||||
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
self::extras('')->removeOrphans('divi');
|
||||
}
|
||||
|
||||
public function testTheIntroAndExtrasOfOtherProfilesAreEmpty(): void
|
||||
{
|
||||
self::assertSame('', self::extraHtml(ReleaseMatrices::released('5.3.0'), null, 'pods'));
|
||||
self::assertSame('', self::introHtml(self::mixedStatus(), null, 'pods'));
|
||||
}
|
||||
|
||||
public function testTheExtrasExplainTheAutomationTheFilterAndTheVisualBuilderWithAReport(): void
|
||||
{
|
||||
$html = self::extraHtml(ReleaseMatrices::released('5.3.0'), self::extras('5.3.0', null, true));
|
||||
|
||||
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);
|
||||
self::assertStringContainsString('Visual Builder', $html);
|
||||
}
|
||||
|
||||
public function testTheVisualBuilderHintNeedsAReport(): void
|
||||
{
|
||||
$html = self::extraHtml(ReleaseMatrices::released('5.3.0'));
|
||||
|
||||
self::assertStringNotContainsString('Visual Builder', $html);
|
||||
self::assertStringContainsString('gibt LIT MCP jede Fähigkeit einzeln frei', $html);
|
||||
}
|
||||
|
||||
public function testTheIntroSaysWhatTheCheckDoes(): void
|
||||
{
|
||||
$html = self::introHtml(ReleaseMatrices::released('5.3.0'));
|
||||
|
||||
self::assertStringContainsString('legt eine private Testseite an, prüft Lesen und Schreiben und löscht sie wieder', $html);
|
||||
}
|
||||
|
||||
public function testAReleasedVersionNeedsNoNextStep(): void
|
||||
{
|
||||
self::assertStringNotContainsString('notice notice-', self::introHtml(ReleaseMatrices::released('5.3.0')));
|
||||
}
|
||||
|
||||
public function testAnIncompatibleVersionNamesThePluginUpdateAsTheNextStep(): void
|
||||
{
|
||||
$html = self::introHtml(self::mixedStatus());
|
||||
|
||||
self::assertStringContainsString('notice notice-error inline', $html);
|
||||
self::assertStringContainsString('Plugin-Update', $html);
|
||||
self::assertStringNotContainsString('lesen nur', $html);
|
||||
}
|
||||
|
||||
public function testAnUntestedVersionNamesTheCheckAsTheNextStep(): void
|
||||
{
|
||||
$html = self::introHtml(ReleaseMatrices::uniform(ReleaseState::Untested));
|
||||
|
||||
self::assertStringContainsString('notice notice-warning inline', $html);
|
||||
self::assertStringContainsString('Divi 5.3.0 ist noch nicht geprüft. Starte die Prüfung', $html);
|
||||
}
|
||||
|
||||
public function testTheExplanationNamesTheSourceOfAReleasedVersion(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan <Admin>']);
|
||||
Functions\when('get_option')->justReturn('Y-m-d');
|
||||
Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time));
|
||||
|
||||
$code = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('code')));
|
||||
$auto = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('auto')));
|
||||
$database = self::explanationOf(ReleaseStatus::of(
|
||||
'5.3.0',
|
||||
MatrixFixtures::released('database'),
|
||||
['user_id' => 7, 'released_at' => '2026-10-01T08:00:00+00:00']
|
||||
));
|
||||
|
||||
self::assertSame('Diese Version ist im Plugin als getestet hinterlegt.', $code);
|
||||
self::assertStringStartsWith('Automatisch freigegeben: Alle Divi-Stellen', $auto);
|
||||
self::assertSame('Auf dieser Website freigegeben am 2026-10-01 von Jan <Admin>.', $database);
|
||||
}
|
||||
|
||||
public function testTheExplanationOfADatabaseReleaseWithoutAKnownUserNamesAQuestionMark(): void
|
||||
{
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
Functions\when('get_option')->justReturn('Y-m-d');
|
||||
Functions\when('date_i18n')->alias(static fn (string $format, int $time): string => gmdate($format, $time));
|
||||
|
||||
$text = self::explanationOf(ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), ['user_id' => 7]));
|
||||
|
||||
self::assertStringEndsWith('von ?.', $text);
|
||||
}
|
||||
|
||||
public function testTheExplanationHasOwnTextsForUntestedAndIncompatible(): void
|
||||
{
|
||||
$untested = self::explanationOf(ReleaseMatrices::uniform(ReleaseState::Untested));
|
||||
$incompatible = self::explanationOf(self::mixedStatus());
|
||||
|
||||
self::assertStringContainsString('Diese Version ist nicht geprüft. Die Tools der ungetesteten Fähigkeiten', $untested);
|
||||
self::assertStringContainsString('bis ein Plugin-Update von LIT MCP eingespielt ist', $incompatible);
|
||||
}
|
||||
|
||||
public function testTheExplanationOfOtherProfilesStaysGeneric(): void
|
||||
{
|
||||
self::assertSame('generisch', self::explanationOf(self::mixedStatus(), 'pods'));
|
||||
}
|
||||
|
||||
/** @return list<array{heading: string, cell: callable, code?: bool}> */
|
||||
private static function columns(DiviAdminExtras $extras, string $id = 'divi'): array
|
||||
{
|
||||
return $extras->columns([], $id);
|
||||
}
|
||||
|
||||
/** @return list<list<string>> The cells of one capability across the three columns. */
|
||||
private static function cellsOf(DiviFeature $feature, array $columns, ReleaseStatus $status): array
|
||||
{
|
||||
return array_map(static fn (array $column): array => ($column['cell'])($feature, $status->matrix), $columns);
|
||||
}
|
||||
|
||||
public function testTheCapabilityTableGetsSourceProblemsAndAffectedTools(): 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]),
|
||||
]);
|
||||
$columns = self::columns(self::extras('5.3.0', $overview));
|
||||
$status = self::mixedStatus();
|
||||
|
||||
self::assertSame(['Quelle', 'Probleme', 'Betroffene Tools'], array_column($columns, 'heading'));
|
||||
self::assertTrue($columns[2]['code']);
|
||||
self::assertSame(
|
||||
[[], ['Seiten: Signatur von et_pb_x() <geändert>'], ['lit-divi-insert']],
|
||||
self::cellsOf(DiviFeature::PageWrite, $columns, $status)
|
||||
);
|
||||
self::assertSame(
|
||||
[['Automatisch geprüft'], [], ['lit-divi-insert']],
|
||||
self::cellsOf(DiviFeature::PageRead, $columns, $status)
|
||||
);
|
||||
self::assertSame(
|
||||
['lit/divi-presets-assign'],
|
||||
self::cellsOf(DiviFeature::PresetWrite, $columns, $status)[2]
|
||||
);
|
||||
}
|
||||
|
||||
public function testTheColumnsNameEverySourceInWordsAndKeepTheOthersAndOtherProfiles(): void
|
||||
{
|
||||
// One column set per table: it keeps the rows of the first matrix it sees.
|
||||
$code = self::cellsOf(DiviFeature::PageRead, self::columns(self::extras()), ReleaseMatrices::released('5.3.0'));
|
||||
$database = self::cellsOf(
|
||||
DiviFeature::PageRead,
|
||||
self::columns(self::extras()),
|
||||
ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'))
|
||||
);
|
||||
|
||||
self::assertSame(['Im Plugin als getestet hinterlegt'], $code[0]);
|
||||
self::assertSame(['Von dir auf dieser Website freigegeben'], $database[0]);
|
||||
self::assertSame([['heading' => 'x']], self::extras()->columns([['heading' => 'x']], 'pods'));
|
||||
self::assertCount(4, self::extras()->columns([['heading' => 'x']], 'divi'));
|
||||
}
|
||||
|
||||
public function testTheColumnsReadTheOverviewOncePerTable(): void
|
||||
{
|
||||
$calls = 0;
|
||||
$overview = new FeatureOverview(static function () use (&$calls): array {
|
||||
++$calls;
|
||||
|
||||
return [];
|
||||
});
|
||||
$columns = self::columns(self::extras('5.3.0', $overview));
|
||||
$status = ReleaseMatrices::released('5.3.0');
|
||||
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
self::cellsOf($feature, $columns, $status);
|
||||
}
|
||||
|
||||
self::assertSame(1, $calls);
|
||||
}
|
||||
}
|
||||
@@ -1,561 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Admin;
|
||||
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Admin\AdminPostAction;
|
||||
use LIT\MCP\Compatibility\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Compatibility\CheckReport;
|
||||
use LIT\MCP\Compatibility\CheckReportStore;
|
||||
use LIT\MCP\Compatibility\CheckStep;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviCheckSteps;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureOverview;
|
||||
use LIT\MCP\Compatibility\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Compatibility\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\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use WP_Post;
|
||||
|
||||
final class DiviTabTest extends TestCase
|
||||
{
|
||||
private ?string $redirect = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->redirect = null;
|
||||
Functions\stubEscapeFunctions();
|
||||
Functions\stubTranslationFunctions();
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('check_admin_referer')->justReturn(1);
|
||||
Functions\when('get_current_user_id')->justReturn(1);
|
||||
Functions\when('wp_safe_redirect')->alias(function (string $url): bool {
|
||||
$this->redirect = $url;
|
||||
|
||||
return true;
|
||||
});
|
||||
Functions\when('admin_url')->alias(static fn (string $path = ''): string => 'https://k.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($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
|
||||
{
|
||||
$steps = array_map(
|
||||
static fn (string $step): CheckStep => new CheckStep($step, $green ? [] : ['rot']),
|
||||
[CheckStep::MANIFEST, CheckStep::MIRRORS, DiviCheckSteps::CATALOG, DiviCheckSteps::FORMAT, DiviCheckSteps::WRITE]
|
||||
);
|
||||
|
||||
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,
|
||||
?FeatureOverview $overview = null
|
||||
): DiviTab {
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->allows('latest')->andReturn($report);
|
||||
|
||||
return new DiviTab(
|
||||
$reports,
|
||||
$releases,
|
||||
$log ?? FakeChangeLog::forbidden(),
|
||||
new AdminPostAction(static function (): void {
|
||||
}),
|
||||
static fn (): string => '5.3.0',
|
||||
static fn (string $version): string => 'key',
|
||||
$overview
|
||||
);
|
||||
}
|
||||
|
||||
public function testReleaseNeedsAFreshGreenReport(): void
|
||||
{
|
||||
$reports = [null, self::report(false), self::report(true, '5.2.9'), self::report(true, '5.3.0', 'old')];
|
||||
foreach ($reports as $report) {
|
||||
$this->redirect = null;
|
||||
$releases = Mockery::mock(ReleaseStore::class);
|
||||
$releases->expects('add')->never();
|
||||
|
||||
$this->tab($report, $releases)->release();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_release_refused', (string) $this->redirect);
|
||||
}
|
||||
}
|
||||
|
||||
public function testReleaseStoresTheEntryAndLogsIt(): void
|
||||
{
|
||||
$releases = Mockery::mock(ReleaseStore::class);
|
||||
$releases->expects('add')->once()->with('5.3.0', Plugin::VERSION, 1, 'r1');
|
||||
$log = new FakeChangeLog();
|
||||
|
||||
$this->tab(self::report(), $releases, $log)->release();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_released', (string) $this->redirect);
|
||||
self::assertSame('lit/divi-release', $log->records[0]['ability']);
|
||||
self::assertStringContainsString('Divi 5.3.0', $log->records[0]['summary']);
|
||||
}
|
||||
|
||||
public function testWithdrawRemovesTheEntryOfTheInstalledVersion(): void
|
||||
{
|
||||
$releases = Mockery::mock(ReleaseStore::class);
|
||||
$releases->expects('withdraw')->once()->with('5.3.0', Plugin::VERSION)->andReturn(true);
|
||||
$log = new FakeChangeLog();
|
||||
|
||||
$this->tab(null, $releases, $log)->withdraw();
|
||||
|
||||
self::assertStringContainsString('lit_mcp_notice=divi_withdrawn', (string) $this->redirect);
|
||||
self::assertSame('lit/divi-release', $log->records[0]['ability']);
|
||||
}
|
||||
|
||||
public function testActionsNeedNonceAndManageOptions(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\expect('wp_die')->twice()->andReturnUsing(static function (): never {
|
||||
throw new \RuntimeException('died');
|
||||
});
|
||||
$releases = Mockery::mock(ReleaseStore::class);
|
||||
$releases->expects('add')->never();
|
||||
$releases->expects('withdraw')->never();
|
||||
$tab = $this->tab(self::report(), $releases);
|
||||
|
||||
foreach (['release', 'withdraw'] as $method) {
|
||||
try {
|
||||
$tab->{$method}();
|
||||
self::fail($method . ' without manage_options');
|
||||
} catch (\RuntimeException $e) {
|
||||
self::assertSame('died', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function testReleaseChecksTheNonce(): void
|
||||
{
|
||||
Functions\when('check_admin_referer')->alias(static function (string $action): never {
|
||||
throw new \RuntimeException('bad nonce for ' . $action);
|
||||
});
|
||||
$releases = Mockery::mock(ReleaseStore::class);
|
||||
$releases->expects('add')->never();
|
||||
|
||||
$this->expectExceptionMessage('bad nonce for lit_mcp_divi_release');
|
||||
$this->tab(self::report(), $releases)->release();
|
||||
}
|
||||
|
||||
/** @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');
|
||||
Functions\when('wp_create_nonce')->justReturn('n');
|
||||
Functions\when('wp_nonce_field')->justReturn('');
|
||||
Functions\when('submit_button')->alias(static function (string $text, string $type = 'primary'): void {
|
||||
echo '<button class="' . $type . '">' . $text . '</button>';
|
||||
});
|
||||
Functions\when('get_userdata')->justReturn((object) ['display_name' => 'Jan']);
|
||||
Functions\when('date_i18n')->justReturn('01.10.2026 10:00');
|
||||
Functions\when('get_option')->justReturn('d.m.Y');
|
||||
|
||||
ob_start();
|
||||
$tab->render();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
public function testUntestedWithAFreshGreenReportOffersTheRelease(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report());
|
||||
|
||||
self::assertStringContainsString('ungetestet', $html);
|
||||
self::assertStringContainsString('Prüfung starten', $html);
|
||||
self::assertStringContainsString('Divi 5.3.0 freigeben', $html);
|
||||
self::assertStringContainsString('name="_wpnonce" value="n"', $html);
|
||||
}
|
||||
|
||||
public function testCheckButtonSaysWhatItDoesAndLeadsWhileNothingElseCanBeDone(): void
|
||||
{
|
||||
$stale = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9'));
|
||||
$fresh = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report());
|
||||
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
||||
|
||||
foreach ([$stale, $fresh, $released] as $html) {
|
||||
self::assertStringContainsString('legt eine private Testseite an', $html);
|
||||
}
|
||||
self::assertStringContainsString('<button class="primary">Prüfung starten</button>', $stale);
|
||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $fresh);
|
||||
self::assertStringContainsString('<button class="primary">Divi 5.3.0 freigeben</button>', $fresh);
|
||||
self::assertStringContainsString('<button class="secondary">Prüfung starten</button>', $released);
|
||||
}
|
||||
|
||||
public function testUntestedWithAStaleReportAsksForANewCheck(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), self::report(true, '5.2.9'));
|
||||
|
||||
self::assertStringNotContainsString('Divi 5.3.0 freigeben', $html);
|
||||
self::assertStringContainsString('Prüfung erneut ausführen', $html);
|
||||
}
|
||||
|
||||
public function testReleasedFromCodeOffersNeitherReleaseNorWithdrawal(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.3.0'), self::report());
|
||||
|
||||
self::assertStringContainsString('freigegeben', $html);
|
||||
self::assertStringContainsString('im Plugin als getestet hinterlegt', $html);
|
||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
||||
}
|
||||
|
||||
public function testAnAutomaticReleaseSaysSoAndOffersNeitherReleaseNorWithdrawal(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::released('5.99.0', ReleaseStatus::SOURCE_AUTO), self::report());
|
||||
|
||||
self::assertStringContainsString('Automatisch freigegeben', $html);
|
||||
self::assertStringNotContainsString('freigeben</button>', $html);
|
||||
self::assertStringNotContainsString('Freigabe zurückziehen', $html);
|
||||
}
|
||||
|
||||
public function testADatabaseReleaseCanBeWithdrawn(): void
|
||||
{
|
||||
$status = ReleaseMatrices::released('5.3.0', ReleaseStatus::SOURCE_DATABASE, [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
]);
|
||||
|
||||
self::assertStringContainsString('Freigabe zurückziehen', $this->rendered($status, self::report()));
|
||||
}
|
||||
|
||||
public function testWithoutDiviShowsOnlyAShortNotice(): void
|
||||
{
|
||||
ReleaseGate::useResolver(static function (): ReleaseStatus {
|
||||
self::fail('resolver called although Divi is not active');
|
||||
});
|
||||
$reports = Mockery::mock(CheckReportStore::class);
|
||||
$reports->expects('latest')->never();
|
||||
$tab = new DiviTab(
|
||||
$reports,
|
||||
Mockery::mock(ReleaseStore::class),
|
||||
FakeChangeLog::forbidden(),
|
||||
new AdminPostAction(static function (): void {
|
||||
}),
|
||||
static fn (): string => '',
|
||||
static fn (string $version): string => 'key'
|
||||
);
|
||||
|
||||
ob_start();
|
||||
$tab->render();
|
||||
$html = (string) ob_get_clean();
|
||||
|
||||
self::assertStringContainsString(
|
||||
'Divi ist auf dieser Website nicht aktiv; die Divi-Freigabe ist ohne Bedeutung.',
|
||||
$html
|
||||
);
|
||||
self::assertStringNotContainsString('Prüfung starten', $html);
|
||||
}
|
||||
|
||||
public function testIncompatibleListsTheProblems(): void
|
||||
{
|
||||
$html = $this->rendered(
|
||||
ReleaseMatrices::uniform(ReleaseState::Incompatible, '5.3.0', ['Theme Builder: Nachbau von x() – y']),
|
||||
null
|
||||
);
|
||||
|
||||
self::assertStringContainsString('inkompatibel', $html);
|
||||
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(ReleaseMatrices::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(ReleaseMatrices::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();
|
||||
}
|
||||
|
||||
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 ReleaseStatus::of('5.3.0', $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 Tools</th>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td data-label="Fähigkeit">Seiten schreiben</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-dismiss"><span class="dashicons dashicons-dismiss" aria-hidden="true"></span> <strong>inkompatibel</strong></span></td><td data-label="Quelle">–</td>',
|
||||
$html
|
||||
);
|
||||
self::assertStringContainsString('<td data-label="Fähigkeit">Modulkatalog</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> <strong>ungetestet</strong></span></td>', $html);
|
||||
self::assertStringContainsString(
|
||||
'<td data-label="Fähigkeit">Seiten lesen</td><td data-label="Zustand"><span class="lit-mcp-state lit-mcp-status-yes-alt"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> <strong>freigegeben</strong></span></td><td data-label="Quelle">Automatisch geprüft</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() <geändert>', $html);
|
||||
self::assertStringNotContainsString('<geändert>', $html);
|
||||
}
|
||||
|
||||
public function testTheTableNamesEverySourceInWords(): void
|
||||
{
|
||||
$code = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
||||
$database = $this->rendered(
|
||||
ReleaseStatus::of('5.3.0', MatrixFixtures::released('database'), [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
]),
|
||||
self::report()
|
||||
);
|
||||
|
||||
self::assertStringContainsString('Im Plugin als getestet hinterlegt', $code);
|
||||
self::assertStringContainsString('Von dir auf dieser Website freigegeben', $database);
|
||||
}
|
||||
|
||||
public function testTheTabExplainsTheAutomationAndTheFilter(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::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(
|
||||
ReleaseStatus::of('5.3.0', 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 = ReleaseStatus::of('5.3.0', $matrix, [
|
||||
'released_at' => '2026-10-01T08:00:00+00:00', 'user_id' => 1,
|
||||
]);
|
||||
|
||||
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(ReleaseMatrices::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();
|
||||
}
|
||||
|
||||
public function testNamesTheNextStepAboveTheDetailsAndKeepsAReleasedTableFolded(): void
|
||||
{
|
||||
$incompatible = $this->rendered(self::mixedStatus(), null);
|
||||
$released = $this->rendered(ReleaseMatrices::released('5.3.0'), null);
|
||||
|
||||
self::assertStringContainsString('notice notice-error inline', $incompatible);
|
||||
self::assertStringContainsString('<details class="lit-mcp-divi-details" open>', $incompatible);
|
||||
self::assertStringNotContainsString('notice notice-', $released);
|
||||
self::assertStringContainsString('<details class="lit-mcp-divi-details">', $released);
|
||||
}
|
||||
|
||||
public function testUntestedVersionNamesTheCheckAsTheNextStep(): void
|
||||
{
|
||||
$html = $this->rendered(ReleaseMatrices::uniform(ReleaseState::Untested), null);
|
||||
|
||||
self::assertStringContainsString('notice notice-warning inline', $html);
|
||||
self::assertStringContainsString('Divi 5.3.0 ist noch nicht geprüft. Starte unten die Prüfung', $html);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,7 @@ use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Ability;
|
||||
use LIT\MCP\Abilities\ExposurePolicy;
|
||||
use LIT\MCP\Abilities\ToolAvailability;
|
||||
use LIT\MCP\Admin\AdminTab;
|
||||
use LIT\MCP\Integrations\Divi\Admin\DiviTab;
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\Admin\SettingsPage;
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Core\EnvironmentComponent;
|
||||
@@ -314,11 +313,10 @@ final class DiviIntegrationTest extends TestCase
|
||||
(new DiviIntegration($schema, FakeChangeLog::forbidden(), new ToolAvailability()))->libraryIdsVisible();
|
||||
}
|
||||
|
||||
/** Bootable: Divi's own hooks – upgrader cache reset, check route, tab "Divi" after the plugin's tabs. */
|
||||
/** Bootable: Divi's own hooks – upgrader cache reset, check route, extras in the tab "Compatibility" (no own tab). */
|
||||
public function testBootRegistersDivisOwnHooks(): void
|
||||
{
|
||||
$upgraded = null;
|
||||
$tabs = null;
|
||||
Actions\expectAdded('upgrader_process_complete')->once()->whenHappen(
|
||||
static function (callable $callback) use (&$upgraded): void {
|
||||
$upgraded = $callback;
|
||||
@@ -330,13 +328,13 @@ final class DiviIntegrationTest extends TestCase
|
||||
$changed = $callback;
|
||||
}
|
||||
);
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once()->whenHappen(
|
||||
static function (callable $callback) use (&$tabs): void {
|
||||
$tabs = $callback;
|
||||
}
|
||||
);
|
||||
// The Divi admin extras hook into the admin notices and the tab "Compatibility"; Divi has no tab of its own.
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
||||
Actions\expectAdded('admin_notices')->once();
|
||||
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once();
|
||||
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
||||
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
||||
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);
|
||||
@@ -345,12 +343,6 @@ final class DiviIntegrationTest extends TestCase
|
||||
self::assertIsCallable($changed);
|
||||
$changed('pods');
|
||||
$changed('divi');
|
||||
self::assertIsCallable($tabs);
|
||||
$all = $tabs([$base]);
|
||||
|
||||
self::assertCount(2, $all);
|
||||
self::assertSame($base, $all[0]);
|
||||
self::assertInstanceOf(DiviTab::class, $all[1]);
|
||||
}
|
||||
|
||||
public function testTheFeatureOverviewFindsTheToolsOfAllIntegrations(): void
|
||||
|
||||
@@ -77,7 +77,7 @@ final class ReleaseStatusTest extends TestCase
|
||||
|
||||
self::assertSame(
|
||||
'Achtung: Divi 5.3.0 ist auf dieser Website für „Seiten schreiben“ noch nicht für LIT MCP freigegeben. '
|
||||
. 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Divi die Prüfung ausführt '
|
||||
. 'Das ist gesperrt, bis ein Administrator unter Einstellungen → LIT MCP → Compatibility die Prüfung ausführt '
|
||||
. 'und die Version freigibt. Es wurde nichts geändert.',
|
||||
$payload['message']
|
||||
);
|
||||
|
||||
@@ -53,12 +53,15 @@ final class PluginTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** Only the Divi integration has boot steps (upgrader hook, compat listener, tab); a second boot registers nothing twice. */
|
||||
/** Only Divi has boot steps (upgrader hook, compat listener, admin extras); a second boot registers nothing twice. */
|
||||
public function testTheBootableIntegrationsBootExactlyOnce(): void
|
||||
{
|
||||
Actions\expectAdded('upgrader_process_complete')->once();
|
||||
Actions\expectAdded('lit_mcp_compat_changed')->once();
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->once();
|
||||
Filters\expectAdded(SettingsPage::TABS_FILTER)->never();
|
||||
Actions\expectAdded(CompatibilityTab::LOAD_ACTION)->once();
|
||||
Actions\expectAdded(CompatibilityTab::EXTRA_ACTION)->once();
|
||||
Filters\expectAdded(CompatibilityTab::COLUMNS_FILTER)->once();
|
||||
$registry = self::registry();
|
||||
|
||||
$registry->boot();
|
||||
|
||||
Reference in New Issue
Block a user