refactor: Kern ohne Divi – Ports BuilderDetector, MediaUsageSource, EnvironmentComponent und ausgeschlossene Post-Typen im ContentSchema; Divi-Implementierungen und Verdrahtung über DiviIntegration/Plugin; CoreIndependenceTest

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 21:49:28 +02:00
co-authored by Claude Sonnet 5.5
parent abeb4b0eed
commit 06148fb8c1
33 changed files with 952 additions and 154 deletions
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Architecture;
use FilesystemIterator;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* The generic core knows no Divi class: nothing below the CORE_PATHS imports or names a class of
* LIT\MCP\Integrations\Divi. Divi plugs in through ports (AbilityGuard, ToolRefusal, BuilderDetector,
* MediaUsageSource, EnvironmentComponent, ContentSchema's excluded post types) that Plugin.php – the composition
* root, outside these paths – wires up. Divi's own names (et_* …) are DiviBoundaryTest's business, not this test's.
*/
final class CoreIndependenceTest extends TestCase
{
/** Directories below src/ that must not use Divi classes. */
private const CORE_PATHS = [
'Abilities',
'Integrations/Core',
'Integrations/WordPress',
'Integrations/Media',
'Admin',
'Guides',
'Server',
];
/** Files inside CORE_PATHS that may use Divi classes, each with its reason. */
private const EXCEPTIONS = [
'Admin/DiviTab.php' => 'The settings tab "Divi" is Divi UI: release state, admin check and release/withdraw.',
];
public function testTheCoreUsesNoDiviClass(): void
{
$offenders = [];
foreach (self::coreFiles() as $relative => $path) {
if (isset(self::EXCEPTIONS[$relative])) {
continue;
}
$names = self::diviNames((string) file_get_contents($path));
if ($names !== []) {
$offenders[] = $relative . ': ' . implode(', ', $names);
}
}
self::assertSame([], $offenders, "Der Kern darf keine Divi-Klasse benutzen (Port über Plugin.php verdrahten):\n"
. implode("\n", $offenders));
}
public function testEveryExceptionExistsAndStillNeedsIt(): void
{
$files = self::coreFiles();
foreach (array_keys(self::EXCEPTIONS) as $relative) {
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – Ausnahme streichen');
self::assertNotSame(
[],
self::diviNames((string) file_get_contents($files[$relative])),
$relative . ' braucht die Ausnahme nicht mehr – streichen'
);
}
}
public function testTheScanFindsImportsQualifiedNamesGroupUsesAndClassStrings(): void
{
$source = <<<'PHP'
<?php
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
use LIT\MCP\Integrations\{Media\Finder, Divi\Pages\DiviPages};
$a = \LIT\MCP\Integrations\Divi\Schema\DiviModules::isAvailable();
$b = 'LIT\\MCP\\Integrations\\Divi\\Design\\DesignSystemReader';
$c = "LIT\\MCP\\Integrations\\Divi\\Library\\LibraryAccess";
PHP;
self::assertSame(
[
'Divi\Pages\DiviPages',
'LIT\MCP\Integrations\Divi\Design\DesignSystemReader',
'LIT\MCP\Integrations\Divi\Library\LibraryAccess',
'LIT\MCP\Integrations\Divi\Release\ReleaseGate',
'LIT\MCP\Integrations\Divi\Schema\DiviModules',
],
self::diviNames($source)
);
}
public function testTheScanIgnoresCommentsOtherNamespacesAndPlainWords(): void
{
$source = <<<'PHP'
<?php
// use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
/** @see \LIT\MCP\Integrations\Divi\Pages\DiviPages */
use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Abilities\DiviMarkup;
$x = 'Divi';
$y = 'Ein Divi-Seitenaufbau';
PHP;
self::assertSame([], self::diviNames($source));
}
/** @return list<string> Sorted, unique names of Divi classes the source uses. */
private static function diviNames(string $source): array
{
$names = [];
foreach (token_get_all($source) as $token) {
if (! is_array($token)) {
continue;
}
$text = match ($token[0]) {
T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED => ltrim($token[1], '\\'),
T_CONSTANT_ENCAPSED_STRING => ltrim(str_replace('\\\\', '\\', substr($token[1], 1, -1)), '\\'),
default => '',
};
if (preg_match('/^(?:LIT\\\\MCP\\\\Integrations\\\\)?Divi\\\\[A-Za-z0-9_\\\\]+$/', $text) === 1) {
$names[] = $text;
}
}
$names = array_values(array_unique($names));
sort($names);
return $names;
}
/** @return array<string, string> Path below src/ => absolute path of every PHP file in CORE_PATHS. */
private static function coreFiles(): array
{
$root = dirname(__DIR__, 3) . '/src/';
$files = [];
foreach (self::CORE_PATHS as $directory) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root . $directory, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->getExtension() === 'php') {
$files[substr((string) $file->getPathname(), strlen($root))] = (string) $file->getPathname();
}
}
}
ksort($files);
return $files;
}
}
@@ -4,8 +4,11 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Core;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Core\CoreIntegration;
use LIT\MCP\Integrations\Core\EnvironmentComponent;
use LIT\MCP\Tests\Unit\TestCase;
use WP_User;
final class CoreIntegrationTest extends TestCase
{
@@ -20,4 +23,23 @@ final class CoreIntegrationTest extends TestCase
array_map(static fn ($a) => $a->name(), $core->abilities())
);
}
public function testSiteStatusReportsTheComponentsOfTheIntegrations(): void
{
Functions\when('get_bloginfo')->justReturn('x');
Functions\when('home_url')->justReturn('https://kunde.de');
Functions\when('wp_get_current_user')->justReturn(new WP_User(3, ['editor']));
Functions\when('get_option')->justReturn(['editor']);
$component = new class implements EnvironmentComponent {
public function describe(): array
{
return ['id' => 'builder', 'active' => true, 'version' => '9', 'problems' => [], 'release' => null];
}
};
$core = new CoreIntegration(dirname(__DIR__, 3) . '/Fixtures/guides', [$component]);
$status = $core->abilities()[1]->execute([]);
self::assertSame(['builder', 'pods', 'the-events-calendar'], array_column($status['components'], 'id'));
}
}
@@ -4,44 +4,51 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Core;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Core\EnvironmentComponent;
use LIT\MCP\Integrations\Core\EnvironmentProbe;
use LIT\MCP\Integrations\Divi\DiviCompatibility;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class EnvironmentProbeTest extends TestCase
{
public function testDiviComponentCarriesTheCompatibilityFindings(): void
private function component(string $id, array $problems = []): EnvironmentComponent
{
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn('Divi');
$theme->allows('parent')->andReturn(false);
$theme->allows('get')->with('Version')->andReturn('5.2.1');
Functions\when('wp_get_theme')->justReturn($theme);
return new class ($id, $problems) implements EnvironmentComponent {
public function __construct(private string $id, private array $problems)
{
}
$probe = new EnvironmentProbe(new DiviCompatibility(['GlobalLayout' => static fn (): bool => false]));
$components = array_column($probe->components(), null, 'id');
self::assertSame(['GlobalLayout'], $components['divi']['problems']);
self::assertSame([], $components['pods']['problems']);
public function describe(): array
{
return [
'id' => $this->id,
'active' => true,
'version' => '1.2.3',
'problems' => $this->problems,
'release' => ['state' => 'released'],
];
}
};
}
public function testDiviComponentCarriesTheReleaseStatus(): void
public function testGivenComponentsComeFirstInTheOrderGivenFollowedByPodsAndTheEventsCalendar(): void
{
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn('Divi');
$theme->allows('parent')->andReturn(false);
$theme->allows('get')->with('Version')->andReturn('5.2.1');
Functions\when('wp_get_theme')->justReturn($theme);
$probe = new EnvironmentProbe([$this->component('builder', ['Etwas fehlt']), $this->component('shop')]);
$components = array_column(
(new EnvironmentProbe(new DiviCompatibility([])))->components(),
null,
'id'
);
$components = $probe->components();
self::assertSame('released', $components['divi']['release']['state'] ?? null);
self::assertNull($components['pods']['release'] ?? null);
self::assertSame(['builder', 'shop', 'pods', 'the-events-calendar'], array_column($components, 'id'));
self::assertSame(['Etwas fehlt'], $components[0]['problems']);
self::assertSame(['state' => 'released'], $components[1]['release']);
}
public function testWithoutComponentsOnlyPodsAndTheEventsCalendarAreReported(): void
{
$components = array_column((new EnvironmentProbe())->components(), null, 'id');
self::assertSame(['pods', 'the-events-calendar'], array_keys($components));
self::assertFalse($components['pods']['active'], 'no Pods in unit tests');
self::assertSame([], $components['pods']['problems']);
self::assertNull($components['pods']['release']);
self::assertNull($components['the-events-calendar']['version']);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\DiviBuilderDetector;
use LIT\MCP\Integrations\WordPress\BuilderDetector;
use LIT\MCP\Tests\Unit\TestCase;
final class DiviBuilderDetectorTest extends TestCase
{
public function testIsABuilderDetectorThatFollowsDiviFiveBeingLoaded(): void
{
$detector = new DiviBuilderDetector();
self::assertInstanceOf(BuilderDetector::class, $detector);
self::assertFalse($detector->isAvailable(), 'no Divi classes in unit tests');
}
public function testALayoutIsMarkedByDivisBuilderFlag(): void
{
Functions\when('get_post_meta')->alias(
static fn (int $postId, string $key): string => $postId === 7 && $key === '_et_pb_use_builder' ? 'on' : ''
);
$detector = new DiviBuilderDetector();
self::assertTrue($detector->usesBuilder(7));
self::assertFalse($detector->usesBuilder(8));
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
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\Tests\Unit\TestCase;
use Mockery;
final class DiviEnvironmentComponentTest extends TestCase
{
private function theme(string $template, string $version = '5.2.1'): void
{
$theme = Mockery::mock();
$theme->allows('get_template')->andReturn($template);
$theme->allows('parent')->andReturn(false);
$theme->allows('get')->with('Version')->andReturn($version);
Functions\when('wp_get_theme')->justReturn($theme);
}
public function testIsAnEnvironmentComponent(): void
{
self::assertInstanceOf(EnvironmentComponent::class, new DiviEnvironmentComponent());
}
public function testCarriesTheCompatibilityFindings(): void
{
$this->theme('Divi');
$component = new DiviEnvironmentComponent(
new DiviCompatibility(['GlobalLayout' => static fn (): bool => false])
);
$described = $component->describe();
self::assertSame('divi', $described['id']);
self::assertTrue($described['active']);
self::assertSame('5.2.1', $described['version']);
self::assertSame(['GlobalLayout'], $described['problems']);
}
public function testCarriesTheReleaseStatus(): void
{
$this->theme('Divi');
$described = (new DiviEnvironmentComponent(new DiviCompatibility([])))->describe();
self::assertSame([], $described['problems']);
self::assertSame('released', $described['release']['state'] ?? null);
}
public function testIsInactiveWithoutDiviAndHasNoProblemsOrRelease(): void
{
$this->theme('twentytwentysix', '1.0');
$described = (new DiviEnvironmentComponent(
new DiviCompatibility(['GlobalLayout' => static fn (): bool => false])
))->describe();
self::assertSame(
['id' => 'divi', 'active' => false, 'version' => null, 'problems' => [], 'release' => null],
$described
);
}
}
@@ -6,8 +6,14 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability;
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\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\LibraryFixtures;
@@ -58,6 +64,43 @@ final class DiviIntegrationTest extends TestCase
}
}
/** Spec §3: the ports the core uses (Plugin wires them) are Divi's own implementations. */
public function testProvidesThePortsTheCoreUses(): void
{
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
self::assertInstanceOf(BuilderDetector::class, $integration->builderDetector());
self::assertInstanceOf(DiviBuilderDetector::class, $integration->builderDetector());
$sources = $integration->mediaUsageSources();
self::assertCount(1, $sources);
self::assertInstanceOf(MediaUsageSource::class, $sources[0]);
self::assertInstanceOf(DiviLogoUsage::class, $sources[0]);
self::assertInstanceOf(EnvironmentComponent::class, $integration->environmentComponent());
self::assertInstanceOf(DiviEnvironmentComponent::class, $integration->environmentComponent());
}
/** The content tools leave the same post types alone as before the schema stopped knowing Divi. */
public function testDiviPostTypesAddedToTheSchemaGiveTheFormerExcludedList(): void
{
self::assertSame(
[
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
'et_pb_canvas', '_et_pb_speculation',
],
array_merge(ContentSchema::EXCLUDED, DiviIntegration::internalPostTypes())
);
}
public function testTheSchemaAsksDiviWhetherTheBuilderIsOnForAPostType(): void
{
Functions\when('et_builder_enabled_for_post_type')->alias(static fn (string $type): bool => $type === 'page');
self::assertTrue(DiviIntegration::builderEnabledFor('page'));
self::assertFalse(DiviIntegration::builderEnabledFor('post'));
}
public function testProvidesTheLibraryGroup(): void
{
Functions\when('__')->returnArg();
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\DiviLogoUsage;
use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Tests\Unit\TestCase;
final class DiviLogoUsageTest extends TestCase
{
private const POSSIBLE_LOGO = [
'post_id' => 0,
'title' => 'Divi-Logo',
'post_type' => 'option',
'where' => 'divi_logo',
'certainty' => 'möglicherweise',
];
private function stub(string $logo, string $file = '2026/09/foto-scaled.jpg'): void
{
Functions\when('get_post_meta')->justReturn($file);
Functions\when('get_option')->alias(
static fn (string $name): mixed => $name === 'et_divi' ? ['divi_logo' => $logo] : false
);
}
public function testIsAMediaUsageSource(): void
{
self::assertInstanceOf(MediaUsageSource::class, new DiviLogoUsage());
}
public function testTheLogoThatIsTheFileIsAPossibleUsage(): void
{
$this->stub('https://k.de/wp-content/uploads/2026/09/foto.jpg');
self::assertSame([self::POSSIBLE_LOGO], (new DiviLogoUsage())->usagesOf(33));
}
public function testAGeneratedSizeOfTheFileCountsToo(): void
{
$this->stub('https://k.de/wp-content/uploads/2026/09/foto-300x200.jpg');
self::assertSame([self::POSSIBLE_LOGO], (new DiviLogoUsage())->usagesOf(33));
}
public function testAnotherFileIsNoUsage(): void
{
$this->stub('https://k.de/wp-content/uploads/2026/09/anderes.jpg');
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
}
public function testWithoutALogoOrWithoutAFileThereIsNoUsage(): void
{
$this->stub('');
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
$this->stub('https://k.de/wp-content/uploads/2026/09/foto.jpg', '');
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
Functions\when('get_option')->justReturn(false);
self::assertSame([], (new DiviLogoUsage())->usagesOf(33));
}
}
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Media;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Media\MediaUsageFinder;
use LIT\MCP\Integrations\Media\MediaUsageSource;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
@@ -21,6 +22,7 @@ final class MediaUsageFinderTest extends TestCase
* @param list<object> $fields
* @param array<string, mixed> $options Option name => value.
* @param array<string, mixed> $themeMods Theme mod name => value.
* @param list<MediaUsageSource> $sources
*/
private function finder(
array $contents = [],
@@ -29,7 +31,8 @@ final class MediaUsageFinderTest extends TestCase
array $options = [],
array $themeMods = [],
array $mediaKeys = [],
string $file = '2026/09/foto-scaled.jpg'
string $file = '2026/09/foto-scaled.jpg',
array $sources = []
): MediaUsageFinder {
Functions\when('get_post_meta')->justReturn($file);
Functions\when('get_option')->alias(static fn (string $name) => $options[$name] ?? false);
@@ -70,7 +73,7 @@ final class MediaUsageFinderTest extends TestCase
return [];
});
return new MediaUsageFinder(static fn (): array => $mediaKeys, $db);
return new MediaUsageFinder(static fn (): array => $mediaKeys, $db, $sources);
}
/** @return array<int|string, string> post_id (or title for site usages) => certainty */
@@ -269,23 +272,78 @@ final class MediaUsageFinderTest extends TestCase
self::assertSame(['header_image', 'background_image', 'widget', 'widget', 'widget', 'widget'], array_column($usages, 'where'));
}
public function testUnreadableEntriesHideTheirTitleAndDiviLogoIsPossible(): void
/**
* @param list<array<string, mixed>> $usages
* @param list<int> $asked Collects the attachment IDs the source is asked about.
*/
private static function source(array $usages, array &$asked = []): MediaUsageSource
{
return new class ($usages, $asked) implements MediaUsageSource {
public function __construct(private array $usages, private array &$asked)
{
}
public function usagesOf(int $attachmentId): array
{
$this->asked[] = $attachmentId;
return $this->usages;
}
};
}
public function testUnreadableEntriesHideTheirTitleAndASourceUsageIsListedAfterTheCoreOnes(): void
{
$logo = MediaUsageFinder::siteUsage('Divi-Logo', 'option', 'divi_logo', MediaUsageFinder::POSSIBLE);
$asked = [];
$usages = $this->finder(
[13 => '<img class="wp-image-33">'],
[self::post(20, 'Geist', 'ghost')],
[],
['et_divi' => ['divi_logo' => 'https://k.de/wp-content/uploads/2026/09/foto.jpg']]
[],
[],
[],
'2026/09/foto-scaled.jpg',
[self::source([$logo], $asked)]
)->usages(33);
self::assertSame(
[
['post_id' => 20, 'title' => '(nicht lesbar)', 'post_type' => 'ghost', 'where' => 'featured', 'certainty' => 'sicher'],
['post_id' => 0, 'title' => 'Divi-Logo', 'post_type' => 'option', 'where' => 'divi_logo', 'certainty' => 'möglicherweise'],
['post_id' => 13, 'title' => '(nicht lesbar)', 'post_type' => 'page', 'where' => 'content', 'certainty' => 'sicher'],
[
'post_id' => 20, 'title' => '(nicht lesbar)', 'post_type' => 'ghost',
'where' => 'featured', 'certainty' => 'sicher',
],
$logo,
[
'post_id' => 13, 'title' => '(nicht lesbar)', 'post_type' => 'page',
'where' => 'content', 'certainty' => 'sicher',
],
],
$usages
);
self::assertSame([33], $asked);
}
public function testASourcePlaceIsReportedOnceAndSourcesAreSkippedOnceTheLimitIsReached(): void
{
$first = MediaUsageFinder::siteUsage('Ort', 'option', 'place', MediaUsageFinder::POSSIBLE);
$second = MediaUsageFinder::siteUsage('Ort neu', 'option', 'place', MediaUsageFinder::CERTAIN);
$asked = [];
$usages = $this->finder([], [], [], [], [], [], '2026/09/foto.jpg', [
self::source([$first]),
self::source([$second]),
])->usages(33);
self::assertSame([$second], $usages);
$finder = $this->finder([], [self::post(5, 'Team')], [], [], [], [], '2026/09/foto.jpg', [
self::source([$first], $asked),
]);
self::assertTrue($finder->isUsed(33));
self::assertSame([], $asked, 'a usage was found before: the sources are not asked');
}
public function testIdReferenceWinsOverAFileMatchInTheSameContent(): void
@@ -13,6 +13,7 @@ use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility;
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Tests\Support\FakeBuilderDetector;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Support\SchemaFixture;
use LIT\MCP\Tests\Unit\TestCase;
@@ -46,9 +47,12 @@ final class ContentUpdateAbilityTest extends TestCase
return $schema;
}
private function ability(PostWriter $writer, ?ChangeLog $log = null): ContentUpdateAbility
{
return new ContentUpdateAbility($this->schema(), $writer, $log ?? FakeChangeLog::forbidden());
private function ability(
PostWriter $writer,
?ChangeLog $log = null,
?PostPresenter $presenter = null
): ContentUpdateAbility {
return new ContentUpdateAbility($this->schema(), $writer, $log ?? FakeChangeLog::forbidden(), $presenter);
}
public function testRevIsRequired(): void
@@ -110,11 +114,11 @@ final class ContentUpdateAbilityTest extends TestCase
public function testRawContentOnDiviPagesIsUnsupported(): void
{
Functions\when('get_post_meta')->justReturn('on');
$writer = Mockery::mock(PostWriter::class);
$writer->expects('prepare')->never();
$presenter = new PostPresenter($this->schema(), new FakeBuilderDetector(false, [5]));
$result = $this->ability($writer)
$result = $this->ability($writer, null, $presenter)
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
self::assertToolError($result, 'unsupported');
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
use LIT\MCP\Tests\Support\FakeBuilderDetector;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use stdClass;
@@ -75,19 +76,21 @@ final class PostPresenterTest extends TestCase
public function testDiviContentIsWithheldAndPointsToTheDiviOutline(): void
{
$this->stubLinks();
Functions\when('get_post_meta')->alias(static fn (int $id, string $key) => $key === '_et_pb_use_builder' ? 'on' : '');
Functions\when('get_post_meta')->justReturn('');
Functions\when('get_post_thumbnail_id')->justReturn(0);
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('taxonomies')->andReturn([]);
$schema->allows('fields')->andReturn([]);
$post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_content' => '<!-- wp:divi/section /-->']);
$full = (new PostPresenter($schema))->full($post);
$presenter = new PostPresenter($schema, new FakeBuilderDetector(false, [5]));
$full = $presenter->full($post);
self::assertTrue($full['divi']);
self::assertNull($full['content']);
self::assertNull($full['featured_image']);
self::assertSame([PostPresenter::diviHint(false)], $full['hints']);
self::assertSame([$presenter->diviHint(false)], $full['hints']);
self::assertInstanceOf(stdClass::class, $full['terms']);
self::assertInstanceOf(stdClass::class, $full['fields']);
self::assertSame(Rev::ofPost($post), $full['rev']);
@@ -95,14 +98,35 @@ final class PostPresenterTest extends TestCase
public function testDiviHintNamesTheDiviToolsOnlyWhenDiviIsActive(): void
{
self::assertStringContainsString('lit-divi-outline', PostPresenter::diviHint(true));
self::assertStringContainsString('lit-content-update', PostPresenter::diviHint(true));
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class));
self::assertStringContainsString('lit-divi-outline', $presenter->diviHint(true));
self::assertStringContainsString('lit-content-update', $presenter->diviHint(true));
foreach (['lit-divi-insert', 'lit-divi-update', 'lit-divi-move', 'lit-divi-delete'] as $tool) {
self::assertStringContainsString($tool, PostPresenter::diviHint(true));
self::assertStringContainsString($tool, $presenter->diviHint(true));
}
self::assertStringNotContainsString('vorerst im Divi-Builder', PostPresenter::diviHint(true));
self::assertStringNotContainsString('lit-divi', PostPresenter::diviHint(false));
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, PostPresenter::diviHint());
self::assertStringNotContainsString('vorerst im Divi-Builder', $presenter->diviHint(true));
self::assertStringNotContainsString('lit-divi', $presenter->diviHint(false));
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, $presenter->diviHint());
}
public function testTheBuilderDetectorDecidesWhichNoteApplies(): void
{
$schema = Mockery::mock(ContentSchema::class);
self::assertSame(
PostPresenter::DIVI_NOTE,
(new PostPresenter($schema, new FakeBuilderDetector(true)))->diviNote()
);
self::assertSame(
PostPresenter::DIVI_INACTIVE_NOTE,
(new PostPresenter($schema, new FakeBuilderDetector(false)))->diviNote()
);
self::assertSame(
PostPresenter::DIVI_INACTIVE_NOTE,
(new PostPresenter($schema))->diviNote(),
'without a detector no builder is assumed'
);
}
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
@@ -131,9 +155,21 @@ final class PostPresenterTest extends TestCase
public function testDiviIsAlsoDetectedFromBlockMarkup(): void
{
Functions\when('get_post_meta')->justReturn('');
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class), new FakeBuilderDetector(true, []));
self::assertTrue(PostPresenter::usesDivi(new WP_Post(['post_content' => 'x <!-- wp:divi/text {} /--> y'])));
self::assertFalse(PostPresenter::usesDivi(new WP_Post(['post_content' => '<!-- wp:paragraph -->'])));
self::assertTrue($presenter->usesDivi(new WP_Post(['post_content' => 'x <!-- wp:divi/text {} /--> y'])));
self::assertFalse($presenter->usesDivi(new WP_Post(['post_content' => '<!-- wp:paragraph -->'])));
}
public function testDiviIsDetectedFromTheBuilderFlagOfTheDetector(): void
{
$presenter = new PostPresenter(Mockery::mock(ContentSchema::class), new FakeBuilderDetector(true, [7]));
self::assertTrue($presenter->usesDivi(new WP_Post(['ID' => 7, 'post_content' => '[et_pb_section]'])));
self::assertFalse($presenter->usesDivi(new WP_Post(['ID' => 8, 'post_content' => '[et_pb_section]'])));
self::assertFalse(
(new PostPresenter(Mockery::mock(ContentSchema::class)))->usesDivi(new WP_Post(['ID' => 7])),
'without a detector only block markup counts'
);
}
}
@@ -8,6 +8,7 @@ use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility;
use LIT\MCP\Tests\Support\FakeBuilderDetector;
use LIT\MCP\Tests\Support\SchemaFixture;
use LIT\MCP\Tests\Unit\TestCase;
use WP_Post;
@@ -24,11 +25,11 @@ final class RevisionsListAbilityTest extends TestCase
Functions\when('get_post_meta')->justReturn('');
}
private function ability(): RevisionsListAbility
private function ability(?PostPresenter $presenter = null): RevisionsListAbility
{
$schema = SchemaFixture::managed();
return new RevisionsListAbility($schema);
return new RevisionsListAbility($schema, $presenter);
}
public function testListsRevisionsNewestFirstWithCursor(): void
@@ -60,11 +61,14 @@ final class RevisionsListAbilityTest extends TestCase
811 => new WP_Post(['ID' => 811, 'post_content' => '<p>Normal</p>']),
]);
$result = $this->ability()->execute(['post_id' => 5]);
$presenter = new PostPresenter(SchemaFixture::managed(), new FakeBuilderDetector(true));
$result = $this->ability($presenter)->execute(['post_id' => 5]);
self::assertTrue($result['items'][0]['divi']);
self::assertFalse($result['items'][1]['divi']);
self::assertSame([PostPresenter::diviHint()], $result['hints']);
self::assertSame([$presenter->diviHint()], $result['hints']);
self::assertStringContainsString(PostPresenter::DIVI_NOTE, $result['hints'][0]);
}
public function testRevisionsOfATrashedPostCanStillBeListed(): void
@@ -37,7 +37,7 @@ final class ContentSchemaTest extends TestCase
{
$this->stubPostTypes();
$schema = new ContentSchema([]);
$schema = new ContentSchema([], null, ['et_pb_layout']);
self::assertSame(['lit_person', 'page', 'post'], $schema->postTypeNames());
self::assertTrue($schema->isManaged('page'));
@@ -80,7 +80,8 @@ final class ContentSchemaTest extends TestCase
new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable'),
new FieldDefinition('zz_hidden', 'Verborgen', 'string', 'pods', FieldDefinition::HIDDEN),
])],
static fn (string $type): bool => $type === 'page'
static fn (string $type): bool => $type === 'page',
['et_pb_layout']
);
$withoutFields = $schema->describe('page', false);
@@ -118,17 +119,34 @@ final class ContentSchemaTest extends TestCase
self::assertSame(['logo', 'photo'], (new ContentSchema([$source]))->mediaFieldKeys());
}
/** Spec §3: Divi's post types come from the Katalog adapter – the list itself stays the same. */
public function testExcludedPostTypesStayTheSame(): void
public function testWordPressExcludesItsOwnPostTypesAndIntegrationsAddTheirs(): void
{
$this->stubPostTypes();
self::assertSame(
['et_pb_layout', 'lit_person', 'page', 'post'],
(new ContentSchema([]))->postTypeNames(),
'without an integration the Divi layouts are an ordinary post type'
);
self::assertSame(
['page', 'post'],
(new ContentSchema([], null, ['et_pb_layout', 'lit_person']))->postTypeNames()
);
self::assertSame(
[
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
'et_pb_canvas', '_et_pb_speculation',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
],
ContentSchema::EXCLUDED
);
}
public function testTheBuilderIsNeverOnWithoutACheck(): void
{
self::assertFalse((new ContentSchema([]))->diviEnabled('page'));
$schema = new ContentSchema([], static fn (string $type): bool => $type === 'page');
self::assertTrue($schema->diviEnabled('page'));
self::assertFalse($schema->diviEnabled('post'));
}
}