feat: Theme Builder adapter, template value and per-object access

DiviThemeBuilder wraps Divi's Theme Builder: reads through Divi's functions,
writes layouts/templates through insert_layout/store_template behind a
wp_die guard (edit_others_posts and function checks first, every handler
filter and output buffer restored in finally), list/conditions/enabled
mirrored from Divi's own save. Template: live = attached and enabled,
whatever the post status (ruling Q8/B8). ThemeBuilderAccess: edit_others_posts
plus Divi's theme_builder role, hidden IDs answer like missing ones,
canEditTemplate (B11) and layoutsRefusal (B10). DiviCompatibility checks the
Theme Builder functions and post types (E15).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 15:18:42 +02:00
co-authored by Claude Opus 5.5
parent 82ef69a99c
commit d04a508751
9 changed files with 1515 additions and 0 deletions
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
use PHPUnit\Framework\TestCase;
final class TemplateTest extends TestCase
{
public function testReadsDivisTemplateArray(): void
{
$template = ThemeBuilderFixtures::template();
self::assertSame(1066, $template->id);
self::assertFalse($template->enabled);
self::assertSame(['id' => 1062, 'enabled' => true], $template->layouts['body']);
self::assertSame(['singular:post_type:page:id:1053'], $template->useOn);
self::assertSame(['page'], $template->postTypes());
self::assertFalse($template->isLive());
}
/** Ruling Q8/B8: Divi renders every listed, enabled template whatever its post status – fail closed. */
public function testLiveMeansAttachedAndEnabledWhateverThePostStatus(): void
{
self::assertTrue(ThemeBuilderFixtures::template(1066, ['enabled' => true])->isLive());
self::assertFalse(ThemeBuilderFixtures::template(1066, ['enabled' => true, 'attached' => false])->isLive());
self::assertFalse(ThemeBuilderFixtures::template(1066, ['attached' => true])->isLive());
self::assertTrue(ThemeBuilderFixtures::template(1066, ['enabled' => true, 'status' => 'trash'])->isLive());
self::assertTrue(ThemeBuilderFixtures::template(1066, ['enabled' => true, 'status' => 'draft'])->isLive());
self::assertTrue(ThemeBuilderFixtures::template(1066, ['enabled' => true])->showsLayout('body', 1062));
$hiddenBody = ThemeBuilderFixtures::template(1066, [
'enabled' => true,
'layouts' => [
'header' => ['id' => 1061, 'enabled' => true],
'body' => ['id' => 1062, 'enabled' => false],
'footer' => ['id' => 1063, 'enabled' => true],
],
]);
self::assertFalse($hiddenBody->showsLayout('body', 1062));
self::assertFalse(ThemeBuilderFixtures::template()->showsLayout('body', 1062), 'disabled template');
}
/** Stored as Divi stores it (stage3b/tb-template--person.json): layout IDs and flags as strings. */
public function testReadsDivisStorageTypes(): void
{
$template = Template::fromDivi([
'id' => '2394',
'title' => '[Fixture 3b] Person',
'default' => false,
'enabled' => false,
'layouts' => [
'header' => ['id' => '1061', 'enabled' => true, 'override' => true, 'global' => false],
'body' => ['id' => '2391', 'enabled' => true],
'footer' => ['id' => '0', 'enabled' => true],
],
'use_on' => ['singular:post_type:lit_fixture_person:all'],
'exclude_from' => [],
], 'publish', true, '2026-09-27 12:47:02');
self::assertSame(2394, $template->id);
self::assertSame(['id' => 1061, 'enabled' => true], $template->layouts['header']);
self::assertSame(['id' => 0, 'enabled' => true], $template->layouts['footer']);
self::assertSame(['lit_fixture_person'], $template->postTypes());
self::assertSame('2026-09-27 12:47:02', $template->toArray()['marked_unused']);
}
public function testRevChangesWithEveryField(): void
{
$rev = ThemeBuilderFixtures::template()->rev();
self::assertSame($rev, ThemeBuilderFixtures::template()->rev());
self::assertNotSame($rev, ThemeBuilderFixtures::template(1066, ['enabled' => true])->rev());
self::assertNotSame($rev, ThemeBuilderFixtures::template(1066, ['use_on' => ['homepage']])->rev());
self::assertNotSame($rev, ThemeBuilderFixtures::template(1066, ['attached' => false])->rev());
}
public function testOverlapsNameOtherLiveTemplatesWithSharedConditions(): void
{
$other = ThemeBuilderFixtures::template(1070, [
'enabled' => true,
'title' => 'Seiten',
'use_on' => ['singular:post_type:page:all', 'homepage'],
]);
$disabled = ThemeBuilderFixtures::template(1071, ['use_on' => ['homepage']]);
self::assertSame(
[['id' => 1070, 'title' => 'Seiten', 'conditions' => ['homepage']]],
Template::overlaps([$other, $disabled], 1066, ['homepage', 'search'])
);
self::assertSame([], Template::overlaps([$other], 1070, ['homepage']));
}
}