feat: Pods fields in Theme Builder bodies checked against the template's post types (E12)

Both forms Divi writes are recognized (ruling Q11): custom_meta_<key> and the
dialog form post_meta_key + select_meta_key. Choice fields get a note about the
stored value, relation fields name IDs (F4). Only templates the user may edit
are checked or named; others are counted (Q21 pattern). Warnings only, also in
the dry run of a live body. DynamicFields extracted from lit/divi-dynamic-sources.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 21:02:53 +02:00
co-authored by Claude Opus 5.5
parent a31d4340f9
commit 6be2e65432
15 changed files with 778 additions and 86 deletions
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Pages\DynamicFields;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class DynamicFieldsTest extends TestCase
{
private static function fields(): DynamicFields
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('fields')->with('lit_person')->andReturn([
new FieldDefinition('role', 'Funktion', 'string', 'pods', FieldDefinition::WRITABLE),
new FieldDefinition('level', 'Stufe', 'choice', 'pods', FieldDefinition::WRITABLE),
new FieldDefinition('mentor', 'Mentor', 'relation', 'pods', FieldDefinition::WRITABLE),
]);
return new DynamicFields($schema, static fn (string $type): ?string => 'meta');
}
public function testTextLikeFieldsAreOfferedOthersOmittedWithTheirReason(): void
{
[$offered, $omitted] = self::fields()->offered('lit_person');
self::assertSame(['role', 'level'], array_column($offered, 'key'));
self::assertSame('{{field:role}}', $offered[0]['placeholder']);
self::assertSame(['mentor'], array_column($omitted, 'key'));
}
public function testReasonForOneKey(): void
{
self::assertNull(self::fields()->reason('lit_person', 'role'));
self::assertStringContainsString('nur IDs', (string) self::fields()->reason('lit_person', 'mentor'));
self::assertSame('kein bekanntes Feld dieses Inhaltstyps', self::fields()->reason('lit_person', 'telefon'));
}
/** Answer F4 / stage3b render: a choice field shows its stored value ("senior"), not the label. */
public function testChoiceFieldsGetANoteAboutTheStoredValue(): void
{
self::assertStringContainsString('gespeicherten Wert', (string) self::fields()->note('lit_person', 'level'));
self::assertNull(self::fields()->note('lit_person', 'role'));
self::assertNull(self::fields()->note('lit_person', 'mentor'));
self::assertNull(self::fields()->note('lit_person', 'telefon'));
}
private static function content(string $name, array $settings): string
{
return VariableRef::serialize('content', $name, $settings);
}
/** Ruling Q11 / stage3b finding 12: both forms Divi resolves (DynamicContentOptionPostMetaKey.php:222-262). */
public function testMetaKeyOfReadsBothFormsDiviWrites(): void
{
self::assertSame('role', DynamicFields::metaKeyOf(self::content('custom_meta_role', ['before' => ''])));
self::assertSame('role', DynamicFields::metaKeyOf(self::content('post_meta_key', [
'before' => '',
'after' => '',
'select_meta_key' => 'custom_meta_role',
'meta_key' => '',
'date_format' => 'default',
'custom_date_format' => '',
'enable_html' => 'off',
])));
self::assertSame('telefon', DynamicFields::metaKeyOf(self::content('post_meta_key', ['meta_key' => 'telefon'])));
self::assertSame('telefon', DynamicFields::metaKeyOf(self::content('post_meta_key', [
'select_meta_key' => 'custom_meta_manual_custom_field_value',
'meta_key' => 'telefon',
])));
self::assertSame('telefon', DynamicFields::metaKeyOf(self::content(
'custom_meta_manual_custom_field_value',
['meta_key' => 'telefon']
)));
}
public function testMetaKeyOfIgnoresEverythingElse(): void
{
self::assertNull(DynamicFields::metaKeyOf(self::content('post_title', ['before' => ''])));
self::assertNull(DynamicFields::metaKeyOf(self::content('post_meta_key', ['meta_key' => ''])));
self::assertNull(DynamicFields::metaKeyOf(self::content('post_meta_key', ['select_meta_key' => 'role'])));
self::assertNull(DynamicFields::metaKeyOf(self::content('custom_meta_', [])));
self::assertNull(DynamicFields::metaKeyOf(
'$variable({"type":"color","value":{"name":"custom_meta_role","settings":{}}})$'
));
self::assertNull(DynamicFields::metaKeyOf('custom_meta_role'));
}
/** The real builder output (stage3b body 2391) names role, level and mentor. */
public function testMetaKeysOfTheRealBuilderBody(): void
{
$content = DiviFixtures::markup('stage3b/tb-body--person-body.html');
preg_match_all('/"value":"(\$variable\(.*?\)\$)"/', $content, $matches);
$keys = [];
foreach ($matches[1] as $value) {
$key = DynamicFields::metaKeyOf((string) json_decode('"' . $value . '"'));
if ($key !== null) {
$keys[] = $key;
}
}
self::assertSame(['role', 'level', 'mentor'], $keys);
}
}