Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Pages/DynamicContentMapTest.php
T
janniclas.loosenandClaude Opus 5.5 203d14a2f9 refactor: one VariableRef per composition, design lookups in DesignSystem (H3)
- DiviComposition::references() builds the one VariableRef with the entry's
  live map (S22/S31) and passes it to AttrWriter; Placeholders only needs the
  design system now, Placeholders::references() is gone (no reach-through).
- DesignSystem::colorIdFor()/variableFor() replace the dead colorId()/variable()
  and carry Placeholders' lookup incl. the S13a ambiguity check (byLabel moved,
  messages unchanged); isColorVariable() decides color vs content for {{var:}}.
- DesignSystem::colorReferenceIds() (VariableRef color names) and
  contentVariableIds() (DynamicContentRules design names) replace the two
  inline computations; same lists, same order.
Tests: fixtures/tests get the VariableRef from ComposeFixture::references() or
build it with the map; DesignSystemTest uses the new lookups (+2 tests).

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

156 lines
7.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\Unit\Integrations\Divi\Pages;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException;
final class DynamicContentMapTest extends TestCase
{
public function testFlattensTheRegistryToOptionNameAndFieldTypes(): void
{
$map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
// before/after: rich text (Divi outputs them as HTML; text() would trim "Von ").
self::assertSame(['before' => 'richtext', 'after' => 'richtext'], $map['post_title']);
self::assertSame(
['before' => 'richtext', 'after' => 'richtext', 'date_format' => 'select', 'custom_date_format' => 'text'],
$map['post_date']
);
self::assertSame(['post_id' => 'select_post'], $map['post_link_url_page']);
self::assertSame([], $map['post_link_url']);
self::assertSame('select', $map['post_meta_key']['select_meta_key']);
// Fields without a readable type become '' – VariableRef's strictest branch (scalar, no markup).
self::assertSame(['a' => '', 'b' => ''], $map['broken_fields']);
self::assertSame([], $map['no_fields']);
self::assertArrayNotHasKey('not_an_array', $map);
self::assertArrayNotHasKey(7, $map);
}
public function testLoopOptionsNeverEnterTheMap(): void
{
$map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
self::assertSame([], array_filter(array_keys($map), static fn (string $name): bool => str_starts_with($name, 'loop_')));
}
public function testPlaceholderOptionsAreAlwaysThere(): void
{
// The map replaces VariableRef's default: every {{post:…}} placeholder must stay usable
// with the settings it writes (before/after), also where the registry lacks the option.
$map = DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry());
foreach (Placeholders::POST as $option) {
self::assertArrayHasKey($option, $map, $option);
foreach (array_keys(DynamicContentRules::DEFAULT_DYNAMIC_CONTENT[$option]) as $setting) {
self::assertArrayHasKey($setting, $map[$option], "$option.$setting");
}
}
// Supplemented with the default settings.
self::assertSame(['before' => 'richtext', 'after' => 'richtext'], $map['post_categories']);
// Live fields win.
self::assertSame('select', $map['post_date']['date_format']);
self::assertSame('text', $map['post_excerpt']['words']);
$placeholders = new Placeholders(DiviFixtures::designSystem());
$references = new VariableRef(DiviFixtures::designSystem(), $map);
foreach (array_keys(Placeholders::POST) as $key) {
$reference = $placeholders->resolve('{{post:' . $key . '}}');
self::assertSame(
$reference,
$references->canonical($reference, ComposeFixture::sanitizer()),
$key
);
}
}
public function testTheMapWorksInVariableRef(): void
{
$references = new VariableRef(DiviFixtures::designSystem(), DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry()));
$sanitizer = ComposeFixture::sanitizer();
$ref = static fn (string $name, array $settings): string => VariableRef::serialize('content', $name, $settings);
// An option only the live registry knows.
$site = $ref('site_title', ['before' => 'Willkommen bei ', 'after' => '']);
self::assertSame($site, $references->canonical($site, $sanitizer));
// before/after are cleaned as rich text (kses for editors) and re-serialized; spaces stay.
self::assertSame(
$ref('post_title', ['before' => 'Neu: ', 'after' => '']),
$references->canonical($ref('post_title', ['before' => '<script>x()</script>Neu: ', 'after' => '']), $sanitizer)
);
// Other text fields go through text().
self::assertSame(
$ref('post_excerpt', ['words' => '20']),
$references->canonical($ref('post_excerpt', ['words' => '<b>20</b>']), $sanitizer)
);
// Meta keys still obey S29/S30, also through the live map.
$meta = $ref('post_meta_key', ['select_meta_key' => 'custom_meta__wp_page_template']);
self::assertRejected(static fn () => $references->canonical($meta, $sanitizer), 'Meta-Schlüssel');
$ok = $ref('post_meta_key', ['meta_key' => 'telefon']);
self::assertSame($ok, $references->canonical($ok, $sanitizer));
// enable_html stays forbidden even though the registry lists it.
$html = $ref('post_meta_key', ['enable_html' => 'on']);
self::assertRejected(static fn () => $references->canonical($html, $sanitizer), 'enable_html');
// Loop content: Divi offers it only inside loops – never through the tools.
$loop = $ref('loop_post_title', []);
self::assertRejected(static fn () => $references->canonical($loop, $sanitizer), 'Schleifen');
}
public function testLiveRegistryForThePost(): void
{
$seen = [];
$map = (new DynamicContentMap(static function (int $postId) use (&$seen): array {
$seen[] = $postId;
return DiviFixtures::dynamicContentRegistry();
}))->forPost(42);
self::assertSame([42], $seen);
self::assertSame(['before' => 'richtext', 'after' => 'richtext'], $map['site_title'] ?? null);
}
public function testUnavailableRegistryFallsBackToTheDefault(): void
{
self::assertNull((new DynamicContentMap(static fn (): array => []))->forPost(7), 'empty (recursion guard)');
self::assertNull((new DynamicContentMap(static fn (): string => 'x'))->forPost(7), 'no array');
$log = (string) tempnam(sys_get_temp_dir(), 'lit-log');
$previous = ini_set('error_log', $log);
try {
$map = (new DynamicContentMap(static function (): array {
throw new RuntimeException('filter exploded');
}))->forPost(7);
} finally {
ini_set('error_log', (string) $previous);
}
self::assertNull($map);
self::assertStringContainsString('filter exploded', (string) file_get_contents($log));
// The message only – no stack trace in the log (review M-4).
self::assertStringNotContainsString('#0 ', (string) file_get_contents($log));
self::assertStringNotContainsString('RuntimeException', (string) file_get_contents($log));
unlink($log);
// Without Divi (no DynamicContentOptions class, as in these unit tests).
self::assertNull((new DynamicContentMap())->forPost(7));
}
private static function assertRejected(callable $call, string $fragment): void
{
try {
$call();
self::fail('InvalidArgumentException expected');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString($fragment, $e->getMessage());
}
}
}