Files
WP-LIT-MCP/tests/Support/ThemeBuilderFixtures.php
T
janniclas.loosenandClaude Opus 5.5 b1dd35abd8 feat: Divi tools edit Theme Builder layouts; visible layouts need confirm
TB layouts load through ThemeBuilderAccess (P12), live layouts (attached +
enabled, Q8) need a dry run and confirm (Q9) and get the shared restore point
of VisibleEdits, extracted from GlobalEdits (Q2: backup when revisions are
off, P9 refusals, P14 naming). divi/post-content only in body layouts (F22),
also for nested stored nodes. SaveScope::ThemeBuilderLayout sets Divi flags
and clears site CSS + TB cache.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 15:45:07 +02:00

186 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\Template;
use LIT\MCP\Integrations\Divi\ThemeBuilder\ThemeBuilderAccess;
use Mockery;
use WP_Post;
/**
* The Theme Builder of the tests: the real builder posts of tests/Fixtures/divi/stage3 (live TB post 1064,
* default template 1065, template 1066 with header 1061, body 1062, footer 1063 – both disabled), a store
* answering from Template values and the access built from them. Write methods of the store are real
* unless a test mocks them.
*/
final class ThemeBuilderFixtures
{
public const THEME_BUILDER = 1064;
public const DEFAULT_TEMPLATE = 1065;
public const TEMPLATE = 1066;
public const HEADER = 1061;
public const BODY = 1062;
public const FOOTER = 1063;
/** @var array<int, array{string, string}> Layout ID => [stage3 fixture file, post type]. */
public const LAYOUTS = [
1061 => ['tb-header--globaler-header.html', 'et_header_layout'],
1062 => ['tb-body--body-mit-dynamic-content.html', 'et_body_layout'],
1063 => ['tb-footer--footer.html', 'et_footer_layout'],
];
/** @param array<string, mixed> $fields */
public static function layout(int $id, array $fields = []): WP_Post
{
return new WP_Post(array_merge([
'ID' => $id,
'post_type' => self::LAYOUTS[$id][1],
'post_status' => 'publish',
'post_title' => '[Fixture] ' . $id,
'post_content' => DiviFixtures::markup('stage3/' . self::LAYOUTS[$id][0]),
], $fields));
}
/** @param array<string, mixed> $fields */
public static function templatePost(int $id = self::TEMPLATE, array $fields = []): WP_Post
{
return new WP_Post(array_merge([
'ID' => $id,
'post_type' => DiviThemeBuilder::TEMPLATE,
'post_status' => 'publish',
'post_title' => $id === self::DEFAULT_TEMPLATE ? 'Standard-Website-Template' : '[Fixture] Vorlage Globale Instanzen',
'post_content' => '',
], $fields));
}
/**
* The template as Divi reads it (et_theme_builder_get_template); $overrides replace keys of that
* array and may set status, attached and marked_unused.
*
* @param array<string, mixed> $overrides
*/
public static function template(int $id = self::TEMPLATE, array $overrides = []): Template
{
$base = $id === self::DEFAULT_TEMPLATE
? [
'id' => $id,
'title' => 'Standard-Website-Template',
'default' => true,
'enabled' => false,
'layouts' => [
'header' => ['id' => self::HEADER, 'enabled' => true],
'body' => ['id' => 0, 'enabled' => true],
'footer' => ['id' => 0, 'enabled' => true],
],
'use_on' => [],
'exclude_from' => [],
]
: [
'id' => $id,
'title' => '[Fixture] Vorlage Globale Instanzen',
'default' => false,
'enabled' => false,
'layouts' => [
'header' => ['id' => self::HEADER, 'enabled' => true],
'body' => ['id' => self::BODY, 'enabled' => true],
'footer' => ['id' => self::FOOTER, 'enabled' => true],
],
'use_on' => ['singular:post_type:page:id:1053'],
'exclude_from' => [],
];
return Template::fromDivi(
array_merge($base, array_intersect_key($overrides, $base)),
(string) ($overrides['status'] ?? 'publish'),
(bool) ($overrides['attached'] ?? true),
isset($overrides['marked_unused']) ? (string) $overrides['marked_unused'] : null
);
}
/**
* A store whose live list is $templates, with a TB draft when $draftId > 0, $detached as templates no
* Theme Builder post lists and $byIdOnly as templates template() finds but no list names (e.g. the one a
* test lets storeTemplate() create).
*
* @param list<Template> $templates
* @param list<Template> $detached
* @param list<Template> $byIdOnly
*/
public static function store(
array $templates = [],
int $draftId = 0,
array $detached = [],
array $byIdOnly = []
): DiviThemeBuilder {
$byId = [];
foreach (array_merge($templates, $detached, $byIdOnly) as $template) {
$byId[$template->id] = $template;
}
$store = Mockery::mock(DiviThemeBuilder::class)->makePartial();
$store->allows('liveId')->andReturn(self::THEME_BUILDER);
$store->allows('draftId')->andReturn($draftId);
$store->allows('liveTemplates')->andReturn($templates);
$store->allows('templateIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $templates));
$store->allows('template')->andReturnUsing(static fn (int $id): ?Template => $byId[$id] ?? null);
$store->allows('detachedIds')->andReturn(array_map(static fn (Template $t): int => $t->id, $detached));
$store->allows('flatConditions')->andReturn(self::conditions());
return $store;
}
/** @param list<string> $deniedRoles Divi role options the user lacks. */
public static function access(
array $deniedRoles = [],
bool $writable = true,
?DiviThemeBuilder $store = null,
bool $visible = true
): ThemeBuilderAccess {
return new ThemeBuilderAccess(
$store ?? self::store(),
LibraryFixtures::roles($deniedRoles),
static fn (): bool => $writable,
static fn (): bool => $visible
);
}
/** A loader with the library access of LibraryFixtures and $access (default: full TB rights, empty live list). */
public static function loader(?ThemeBuilderAccess $access = null): DiviPageLoader
{
return new DiviPageLoader(DiviFixtures::schema(), new Parser(), LibraryFixtures::access(), $access ?? self::access());
}
/**
* An excerpt of et_theme_builder_get_flat_template_settings_options() as the adapter returns it
* (live DDEV dump 2026-09-27 plus the Pods type lit_person).
*
* @return array<string, array{label: string, type: ?string, value: ?string}>
*/
public static function conditions(): array
{
return [
'singular:post_type:page:all' => ['label' => 'Alle Seiten', 'type' => null, 'value' => null],
'homepage' => ['label' => 'Startseite', 'type' => null, 'value' => null],
'singular:post_type:page:id:' => ['label' => 'Bestimmte Seiten', 'type' => 'post_type', 'value' => 'page'],
'singular:post_type:page:children:id:' => [
'label' => 'Unterseiten bestimmter Seiten', 'type' => 'post_type', 'value' => 'page',
],
'singular:post_type:lit_person:all' => ['label' => 'Alle Personen', 'type' => null, 'value' => null],
'singular:post_type:lit_person:id:' => [
'label' => 'Bestimmte Personen', 'type' => 'post_type', 'value' => 'lit_person',
],
'archive:taxonomy:category:term:id:' => [
'label' => 'Bestimmte Kategorien', 'type' => 'taxonomy', 'value' => 'category',
],
'archive:user:id:' => ['label' => 'Bestimmte Autoren', 'type' => 'user', 'value' => null],
'archive:user:role:' => ['label' => 'Autoren mit Rolle', 'type' => 'user_role', 'value' => null],
'search' => ['label' => 'Suchergebnisse', 'type' => null, 'value' => null],
'404' => ['label' => '404-Seite', 'type' => null, 'value' => null],
];
}
}