Files
WP-LIT-MCP/tests/Support/ThemeBuilderFixtures.php
T
janniclas.loosenandClaude Opus 5.5 14bc954ecc fix: Theme Builder minors of the final review (M-4, M-5, M-6, M-9, M-10, M-11, M-12 c)
- tb-list applies the detached limit after the edit filter (reads up to 500
  IDs) and notes when more are not listed (M-4); an invalid template_id is
  invalid input, shared with ThemeBuilderAccess::template() (M-5).
- Templates carry their status; tb-list notes that a live template outside
  publish (e.g. trashed) can only be switched off (M-10).
- changeableTemplate() refuses without canUse() first, like tb-list and
  tb-create-template; the layout/template refusal difference is documented
  (M-9).
- tb-create-template reads the default template from editableLiveTemplates()
  (M-11); the condition-ID character set (role slugs) is documented (M-6);
  tb-assign documents that a failed reattach keeps Divi's unused marks
  removed (M-12 c).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 23:12:14 +02:00

193 lines
8.2 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)->byDefault();
$store->allows('detachedIds')->andReturnUsing(static fn (int $limit): array => array_slice(
array_map(static fn (Template $t): int => $t->id, $detached),
0,
$limit
))->byDefault();
$store->allows('isListedAnywhere')->andReturnUsing(
static fn (int $id): bool => in_array($id, array_map(static fn (Template $t): int => $t->id, $templates), true)
)->byDefault();
$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],
];
}
}