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:
@@ -37,8 +37,18 @@ final class DiviCompatibilityTest extends TestCase
|
||||
'et_theme_builder_clear_wp_cache (Theme-Builder-Cache)',
|
||||
'Block-Typ divi/global-layout (Instanzen globaler Module)',
|
||||
'Taxonomien layout_category, layout_tag (Bibliothek)',
|
||||
'Theme Builder: Funktionen et_theme_builder_* und et_()',
|
||||
'Theme Builder: Post-Typen et_template und Layouts',
|
||||
],
|
||||
array_keys(DiviCompatibility::defaults())
|
||||
);
|
||||
}
|
||||
|
||||
public function testChecksTheThemeBuilderInternals(): void
|
||||
{
|
||||
$labels = array_keys(DiviCompatibility::defaults());
|
||||
|
||||
self::assertContains('Theme Builder: Funktionen et_theme_builder_* und et_()', $labels);
|
||||
self::assertContains('Theme Builder: Post-Typen et_template und Layouts', $labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
|
||||
final class DiviThemeBuilderTest extends TestCase
|
||||
{
|
||||
public function testAreaOfTheLayoutTypes(): void
|
||||
{
|
||||
self::assertSame('header', DiviThemeBuilder::areaOf('et_header_layout'));
|
||||
self::assertSame('body', DiviThemeBuilder::areaOf('et_body_layout'));
|
||||
self::assertSame('footer', DiviThemeBuilder::areaOf('et_footer_layout'));
|
||||
self::assertNull(DiviThemeBuilder::areaOf('et_template'));
|
||||
self::assertNull(DiviThemeBuilder::areaOf('page'));
|
||||
}
|
||||
|
||||
/** F17/F18: Divi wp_die()s without edit_others_posts – the adapter never gets there. */
|
||||
public function testWithoutEditOthersPostsDiviIsNeverCalled(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
Functions\expect('et_theme_builder_insert_layout')->never();
|
||||
Functions\expect('et_theme_builder_store_template')->never();
|
||||
|
||||
foreach ([
|
||||
static fn () => (new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt'),
|
||||
static fn () => (new DiviThemeBuilder())->storeTemplate(1064, [
|
||||
'title' => 'Person', 'layouts' => [], 'use_on' => [], 'exclude_from' => [],
|
||||
]),
|
||||
] as $call) {
|
||||
try {
|
||||
$call();
|
||||
self::fail('Divi was called without edit_others_posts');
|
||||
} catch (RuntimeException $e) {
|
||||
self::assertStringContainsString('edit_others_posts', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** While Divi runs, every wp_die handler throws instead (defense in depth); afterwards WordPress' own again. */
|
||||
public function testDiviIsOnlyCalledWithTheRightAndWithoutWpDie(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$seen = [];
|
||||
Functions\when('et_theme_builder_insert_layout')->alias(static function (array $options) use (&$seen): int {
|
||||
$seen = ['options' => $options, 'guarded' => has_filter('wp_die_json_handler') !== false];
|
||||
|
||||
return 2101;
|
||||
});
|
||||
|
||||
self::assertSame(2101, (new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt'));
|
||||
self::assertSame(['post_type' => 'et_body_layout', 'post_title' => 'Person – Inhalt'], $seen['options']);
|
||||
self::assertTrue($seen['guarded'], 'wp_die throws while Divi runs');
|
||||
self::assertFalse(has_filter('wp_die_json_handler'), 'and only then');
|
||||
}
|
||||
|
||||
public function testStoreTemplateIsAlwaysDisabledAndNeverDefault(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('et_')->justReturn(null);
|
||||
$call = [];
|
||||
Functions\when('et_theme_builder_store_template')->alias(
|
||||
static function (int $tb, array $template, bool $allowDefault) use (&$call): int {
|
||||
$call = [$tb, $template, $allowDefault];
|
||||
|
||||
return 2100;
|
||||
}
|
||||
);
|
||||
|
||||
$id = (new DiviThemeBuilder())->storeTemplate(1064, [
|
||||
'title' => 'Person',
|
||||
'layouts' => [
|
||||
'header' => ['id' => 1061, 'enabled' => true],
|
||||
'body' => ['id' => 2101, 'enabled' => true],
|
||||
'footer' => ['id' => 0, 'enabled' => false],
|
||||
],
|
||||
'use_on' => ['singular:post_type:lit_person:all'],
|
||||
'exclude_from' => [],
|
||||
]);
|
||||
|
||||
self::assertSame(2100, $id);
|
||||
self::assertSame(1064, $call[0]);
|
||||
self::assertSame('0', $call[1]['enabled']);
|
||||
self::assertSame('0', $call[1]['default']);
|
||||
self::assertSame('0', $call[1]['autogenerated_title']);
|
||||
self::assertFalse($call[2], 'allow_default');
|
||||
self::assertSame(['id' => 0, 'enabled' => false], $call[1]['layouts']['footer']);
|
||||
}
|
||||
|
||||
/** Mirrors et_theme_builder_api_save(): backup option, append, remove the backup (api.php:223-243). */
|
||||
public function testAttachAppendsLikeDivisSave(): void
|
||||
{
|
||||
Functions\when('get_post_meta')->justReturn(['1065', '1066']);
|
||||
Functions\expect('update_option')->once()->with('et_tb_templates_backup_1064', ['1065', '1066']);
|
||||
Functions\expect('add_post_meta')->once()->with(1064, '_et_template', 2100)->andReturn(501);
|
||||
Functions\expect('delete_option')->once()->with('et_tb_templates_backup_1064');
|
||||
|
||||
(new DiviThemeBuilder())->attach(1064, 2100);
|
||||
}
|
||||
|
||||
public function testSetConditionsReplacesBothListsWithoutDuplicates(): void
|
||||
{
|
||||
$calls = [];
|
||||
Functions\when('delete_post_meta')->alias(static function (int $id, string $key) use (&$calls): bool {
|
||||
$calls[] = ['delete', $key];
|
||||
|
||||
return true;
|
||||
});
|
||||
Functions\when('add_post_meta')->alias(static function (int $id, string $key, string $value) use (&$calls): int {
|
||||
$calls[] = ['add', $key, $value];
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
(new DiviThemeBuilder())->setConditions(
|
||||
2100,
|
||||
['singular:post_type:lit_person:all', 'singular:post_type:lit_person:all'],
|
||||
['singular:post_type:lit_person:id:12']
|
||||
);
|
||||
|
||||
self::assertSame([
|
||||
['delete', '_et_use_on'],
|
||||
['add', '_et_use_on', 'singular:post_type:lit_person:all'],
|
||||
['delete', '_et_exclude_from'],
|
||||
['add', '_et_exclude_from', 'singular:post_type:lit_person:id:12'],
|
||||
], $calls);
|
||||
}
|
||||
|
||||
/** Ruling B16: PHP turns the key '404' into an int; the label and the object type/value are read. */
|
||||
public function testFlatConditionsReadLabelsAndObjectTypes(): void
|
||||
{
|
||||
Functions\when('wp_strip_all_tags')->returnArg();
|
||||
Functions\when('et_theme_builder_get_flat_template_settings_options')->justReturn([
|
||||
'404' => ['id' => '404', 'label' => '404-Seite'],
|
||||
'singular:post_type:page:id:' => [
|
||||
'label' => 'Bestimmte Seiten',
|
||||
'options' => ['type' => 'post_type', 'value' => 'page'],
|
||||
],
|
||||
]);
|
||||
|
||||
$flat = (new DiviThemeBuilder())->flatConditions();
|
||||
|
||||
self::assertSame(['label' => '404-Seite', 'type' => null, 'value' => null], $flat['404']);
|
||||
self::assertSame('post_type', $flat['singular:post_type:page:id:']['type']);
|
||||
self::assertSame('page', $flat['singular:post_type:page:id:']['value']);
|
||||
}
|
||||
|
||||
private const DIE_FILTERS = [
|
||||
'wp_die_handler', 'wp_die_ajax_handler', 'wp_die_json_handler', 'wp_die_jsonp_handler',
|
||||
'wp_die_xml_handler', 'wp_die_xmlrpc_handler',
|
||||
];
|
||||
|
||||
/** A wp_die() inside Divi becomes an exception; every handler filter is gone again afterwards. */
|
||||
public function testWpDieInsideDiviThrowsAndEveryHandlerIsRestored(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$factory = null;
|
||||
Filters\expectAdded('wp_die_handler')->once()->whenHappen(
|
||||
static function (callable $callback, int $priority) use (&$factory): void {
|
||||
self::assertSame(PHP_INT_MAX, $priority);
|
||||
$factory = $callback;
|
||||
}
|
||||
);
|
||||
$guarded = [];
|
||||
Functions\when('et_theme_builder_insert_layout')->alias(static function () use (&$factory, &$guarded): int {
|
||||
foreach (self::DIE_FILTERS as $filter) {
|
||||
$guarded[$filter] = has_filter($filter) !== false;
|
||||
}
|
||||
// What wp_die() does: apply_filters( 'wp_die_handler', '_default_wp_die_handler' ), then call it.
|
||||
$handler = $factory('_default_wp_die_handler');
|
||||
$handler(new WP_Error('divi', 'Sorry, you are not allowed'), '', []);
|
||||
|
||||
return 2101;
|
||||
});
|
||||
|
||||
try {
|
||||
(new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt');
|
||||
self::fail('wp_die did not throw');
|
||||
} catch (RuntimeException $e) {
|
||||
self::assertSame('Divi called wp_die(): Sorry, you are not allowed', $e->getMessage());
|
||||
}
|
||||
|
||||
self::assertSame(array_fill_keys(self::DIE_FILTERS, true), $guarded);
|
||||
foreach (self::DIE_FILTERS as $filter) {
|
||||
self::assertFalse(has_filter($filter), $filter . ' restored');
|
||||
}
|
||||
}
|
||||
|
||||
/** Nothing Divi prints reaches the MCP response, on success and on failure; buffers stay balanced. */
|
||||
public function testOutputOfDiviNeverLeaks(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
$level = ob_get_level();
|
||||
$calls = 0;
|
||||
Functions\when('et_theme_builder_insert_layout')->alias(static function () use (&$calls): int {
|
||||
echo 'Notice: Divi';
|
||||
ob_start();
|
||||
echo 'unclosed buffer';
|
||||
if (++$calls === 2) {
|
||||
throw new RuntimeException('Divi failed');
|
||||
}
|
||||
|
||||
return 2101;
|
||||
});
|
||||
$this->expectOutputString('');
|
||||
|
||||
self::assertSame(2101, (new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt'));
|
||||
self::assertSame($level, ob_get_level());
|
||||
try {
|
||||
(new DiviThemeBuilder())->insertLayout('body', 'Person – Inhalt');
|
||||
self::fail('no exception');
|
||||
} catch (RuntimeException $e) {
|
||||
self::assertSame('Divi failed', $e->getMessage());
|
||||
}
|
||||
self::assertSame($level, ob_get_level());
|
||||
self::assertFalse(has_filter('wp_die_handler'));
|
||||
}
|
||||
|
||||
public function testARefusedLayoutNamesDivisReason(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('et_theme_builder_insert_layout')->justReturn(new WP_Error('db', 'Datenbankfehler'));
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Datenbankfehler');
|
||||
(new DiviThemeBuilder())->insertLayout('header', 'Kopf');
|
||||
}
|
||||
|
||||
public function testAnUnknownAreaIsRefusedBeforeDivi(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\expect('et_theme_builder_insert_layout')->never();
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new DiviThemeBuilder())->insertLayout('sidebar', 'Seite');
|
||||
}
|
||||
|
||||
/** E15: a Divi function that moved is reported, never called (no fatal error mid-write). */
|
||||
public function testMissingDiviFunctionsAreRefusedBeforeWriting(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\expect('et_theme_builder_insert_layout')->never();
|
||||
Functions\expect('et_theme_builder_store_template')->never();
|
||||
$store = Mockery::mock(DiviThemeBuilder::class)->makePartial();
|
||||
$store->allows('missing')->andReturn(['et_theme_builder_insert_layout', 'et_']);
|
||||
|
||||
foreach ([
|
||||
static fn () => $store->insertLayout('body', 'Person – Inhalt'),
|
||||
static fn () => $store->storeTemplate(1064, [
|
||||
'title' => 'Person', 'layouts' => [], 'use_on' => [], 'exclude_from' => [],
|
||||
]),
|
||||
] as $call) {
|
||||
try {
|
||||
$call();
|
||||
self::fail('Divi was called although it is missing');
|
||||
} catch (RuntimeException $e) {
|
||||
self::assertStringContainsString('missing', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Befund 10 / storage format: _et_template holds each ID once, as a string row. */
|
||||
public function testAttachNeverListsATemplateTwice(): void
|
||||
{
|
||||
Functions\when('get_post_meta')->justReturn(['1065', '2100']);
|
||||
Functions\expect('update_option')->never();
|
||||
Functions\expect('add_post_meta')->never();
|
||||
|
||||
(new DiviThemeBuilder())->attach(1064, 2100);
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testAFailedAttachThrowsAndLeavesNoBackup(): void
|
||||
{
|
||||
Functions\when('get_post_meta')->justReturn(['1065']);
|
||||
Functions\when('update_option')->justReturn(true);
|
||||
Functions\when('add_post_meta')->justReturn(false);
|
||||
Functions\expect('delete_option')->once()->with('et_tb_templates_backup_1064');
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new DiviThemeBuilder())->attach(1064, 2100);
|
||||
}
|
||||
|
||||
public function testDetachRemovesTheStoredRow(): void
|
||||
{
|
||||
Functions\expect('delete_post_meta')->once()->with(1064, '_et_template', '2100')->andReturn(true);
|
||||
|
||||
(new DiviThemeBuilder())->detach(1064, 2100);
|
||||
}
|
||||
|
||||
public function testSetEnabledStoresDivisFlagAndReadsItBack(): void
|
||||
{
|
||||
Functions\expect('update_post_meta')->once()->with(2100, '_et_enabled', '1');
|
||||
Functions\when('get_post_meta')->justReturn('1');
|
||||
(new DiviThemeBuilder())->setEnabled(2100, true);
|
||||
}
|
||||
|
||||
public function testSetEnabledThrowsWhenTheFlagDidNotStick(): void
|
||||
{
|
||||
Functions\when('update_post_meta')->justReturn(false);
|
||||
Functions\when('get_post_meta')->justReturn('1');
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
(new DiviThemeBuilder())->setEnabled(2100, false);
|
||||
}
|
||||
|
||||
public function testTemplateReadsStatusAndUnusedMark(): void
|
||||
{
|
||||
Functions\when('et_theme_builder_get_template')->alias(static fn (int $id): array => $id === 2394 ? [
|
||||
'id' => 2394, 'title' => '[Fixture 3b] Person', 'default' => false, 'enabled' => false,
|
||||
'layouts' => ['header' => ['id' => 1061, 'enabled' => true]], 'use_on' => [], 'exclude_from' => [],
|
||||
] : []);
|
||||
Functions\when('get_post_meta')->justReturn('2026-09-27 12:47:02');
|
||||
Functions\when('get_post_status')->justReturn('publish');
|
||||
|
||||
$template = (new DiviThemeBuilder())->template(2394, false);
|
||||
self::assertNotNull($template);
|
||||
self::assertSame('2026-09-27 12:47:02', $template->markedUnused);
|
||||
self::assertFalse($template->attached);
|
||||
self::assertNull((new DiviThemeBuilder())->template(7, true));
|
||||
}
|
||||
}
|
||||
@@ -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']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use WP_Post;
|
||||
|
||||
final class ThemeBuilderAccessTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Functions\when('get_post_type_object')->justReturn(null);
|
||||
}
|
||||
|
||||
/** Spec §4.5 + F12/F23: edit_others_posts and Divi's role option "Theme Builder". */
|
||||
public function testUsingTheThemeBuilderNeedsEditOthersPostsAndTheDiviRole(): void
|
||||
{
|
||||
LibraryFixtures::given([]);
|
||||
self::assertNull(ThemeBuilderFixtures::access()->canUse());
|
||||
self::assertToolError(ThemeBuilderFixtures::access(['theme_builder'])->canUse(), 'forbidden');
|
||||
self::assertFalse(ThemeBuilderFixtures::access(['theme_builder'])->visible());
|
||||
self::assertFalse(ThemeBuilderFixtures::access([], true, null, false)->visible());
|
||||
|
||||
LibraryFixtures::given([], ['edit_others_posts']);
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
||||
}
|
||||
|
||||
/** P12 pattern: without access a layout ID answers exactly like a missing one. */
|
||||
public function testLayoutsAreHiddenLikeMissingIdsWithoutAccess(): void
|
||||
{
|
||||
$body = ThemeBuilderFixtures::layout(1062);
|
||||
LibraryFixtures::given([$body, LibraryFixtures::page('')]);
|
||||
|
||||
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->layout(1062, false));
|
||||
self::assertNull(ThemeBuilderFixtures::access()->layout(7, false), 'a page is no layout');
|
||||
$missing = ThemeBuilderFixtures::access(['theme_builder'])->layout(1062, false);
|
||||
$hidden = ThemeBuilderFixtures::access([], true, null, false)->layout(1062, false);
|
||||
self::assertToolError($missing, 'not_found');
|
||||
self::assertSame(self::clientError($missing), self::clientError(\LIT\MCP\Integrations\WordPress\PostGuard::notFound(1062)));
|
||||
self::assertToolError($hidden, 'not_found');
|
||||
|
||||
LibraryFixtures::given([$body], ['edit_post']);
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->layout(1062, false), 'forbidden');
|
||||
}
|
||||
|
||||
public function testWritingALayoutNeedsTheGroupAndAPublishedLayout(): void
|
||||
{
|
||||
LibraryFixtures::given([ThemeBuilderFixtures::layout(1062), ThemeBuilderFixtures::layout(1063, ['post_status' => 'trash'])]);
|
||||
|
||||
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->layout(1062, true));
|
||||
self::assertToolError(ThemeBuilderFixtures::access([], false)->layout(1062, true), 'forbidden');
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->layout(1063, true), 'validation_failed');
|
||||
}
|
||||
|
||||
public function testTemplatesAreTheOnlyTemplateIds(): void
|
||||
{
|
||||
LibraryFixtures::given([ThemeBuilderFixtures::templatePost(), ThemeBuilderFixtures::layout(1062)]);
|
||||
|
||||
self::assertInstanceOf(WP_Post::class, ThemeBuilderFixtures::access()->template(1066, true));
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->template(1062, false), 'not_found');
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->template(999, false), 'not_found');
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->template('x', false), 'validation_failed');
|
||||
self::assertToolError(ThemeBuilderFixtures::access(['theme_builder'])->template(1066, false), 'not_found');
|
||||
self::assertToolError(ThemeBuilderFixtures::access([], false)->template(1066, true), 'forbidden');
|
||||
}
|
||||
|
||||
public function testCreatingNeedsPublishRightsAndTheGroup(): void
|
||||
{
|
||||
LibraryFixtures::given([]);
|
||||
self::assertNull(ThemeBuilderFixtures::access()->canCreate());
|
||||
self::assertToolError(ThemeBuilderFixtures::access([], false)->canCreate(), 'forbidden');
|
||||
|
||||
LibraryFixtures::given([], ['publish_posts']);
|
||||
self::assertToolError(ThemeBuilderFixtures::access()->canCreate(), 'forbidden');
|
||||
}
|
||||
|
||||
/** E7: an unsaved Theme Builder session would later replace the template list. */
|
||||
public function testAThemeBuilderDraftBlocksWrites(): void
|
||||
{
|
||||
self::assertNull(ThemeBuilderFixtures::access()->draftRefusal());
|
||||
$blocked = ThemeBuilderFixtures::access([], true, ThemeBuilderFixtures::store([], 999))->draftRefusal();
|
||||
self::assertToolError($blocked, 'unsupported');
|
||||
self::assertStringContainsString('Entwurf', self::clientError($blocked)['message']);
|
||||
}
|
||||
|
||||
/** Ruling Q8/B11: tb-list names a template's ID only to a user who may edit it. */
|
||||
public function testTemplateIdsAreNamedOnlyToEditors(): void
|
||||
{
|
||||
LibraryFixtures::given([], []);
|
||||
self::assertTrue(ThemeBuilderFixtures::access()->canEditTemplate(1066));
|
||||
self::assertFalse(ThemeBuilderFixtures::access(['theme_builder'])->canEditTemplate(1066));
|
||||
self::assertFalse(ThemeBuilderFixtures::access([], true, null, false)->canEditTemplate(1066));
|
||||
|
||||
LibraryFixtures::given([], ['edit_post']);
|
||||
self::assertFalse(ThemeBuilderFixtures::access()->canEditTemplate(1066));
|
||||
}
|
||||
|
||||
/** Ruling Q8/B10: reattach/activate only with published layouts of the right area (Divi trashes marked ones). */
|
||||
public function testLayoutsMustBePublishedBeforeTheTemplateGoesBackOrLive(): void
|
||||
{
|
||||
LibraryFixtures::given([
|
||||
ThemeBuilderFixtures::layout(1061),
|
||||
ThemeBuilderFixtures::layout(1062),
|
||||
ThemeBuilderFixtures::layout(1063),
|
||||
]);
|
||||
self::assertNull(ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template()));
|
||||
self::assertNull(ThemeBuilderFixtures::access()->layoutsRefusal(
|
||||
ThemeBuilderFixtures::template(ThemeBuilderFixtures::DEFAULT_TEMPLATE)
|
||||
), 'areas without a layout (ID 0) need nothing');
|
||||
|
||||
LibraryFixtures::given([
|
||||
ThemeBuilderFixtures::layout(1061),
|
||||
ThemeBuilderFixtures::layout(1062, ['post_status' => 'trash']),
|
||||
]);
|
||||
$refusal = ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template());
|
||||
self::assertToolError($refusal, 'validation_failed');
|
||||
self::assertSame(['layouts.body', 'layouts.footer'], array_keys(self::errorFields($refusal)));
|
||||
|
||||
LibraryFixtures::given([
|
||||
ThemeBuilderFixtures::layout(1061),
|
||||
ThemeBuilderFixtures::layout(1063, ['ID' => 1062]),
|
||||
ThemeBuilderFixtures::layout(1063),
|
||||
]);
|
||||
$refusal = ThemeBuilderFixtures::access()->layoutsRefusal(ThemeBuilderFixtures::template());
|
||||
self::assertSame(['layouts.body'], array_keys(self::errorFields($refusal)), 'a footer is no body');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user