Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Pages/DynamicContentMapTest.php
T

209 lines
9.9 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\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Tests\Unit\Integrations\Divi\Compose\ComposeFixture;
use LIT\MCP\Tests\Unit\Integrations\Divi\Design\DesignSystemTest;
use LIT\MCP\Tests\Unit\TestCase;
use RuntimeException;
final class DynamicContentMapTest extends TestCase
{
/**
* Shape of DynamicContentOptions::get_options($pageId, 'edit') in Divi 5.2.1 (excerpt of a
* live dump for a page: no post_categories/post_tags – pages have no such taxonomies).
*
* @return array<string, array<string, mixed>>
*/
public static function registry(): array
{
$text = static fn (): array => ['label' => 'x', 'type' => 'text', 'default' => ''];
$option = static fn (string $id, string $group, array $fields): array => [
'id' => $id, 'label' => $id, 'type' => 'text', 'custom' => false, 'group' => $group, 'fields' => $fields,
];
return [
'post_title' => $option('post_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_excerpt' => $option('post_excerpt', 'Default', [
'before' => $text(), 'after' => $text(), 'words' => $text(), 'read_more_label' => $text(),
]),
'post_date' => $option('post_date', 'Default', [
'before' => $text(),
'after' => $text(),
'date_format' => ['label' => 'x', 'type' => 'select', 'default' => 'default'],
'custom_date_format' => $text(),
]),
'post_author' => $option('post_author', 'Default', [
'before' => $text(),
'after' => $text(),
'name_format' => ['type' => 'select'],
'link' => ['type' => 'yes_no_button'],
]),
'site_title' => $option('site_title', 'Default', ['before' => $text(), 'after' => $text()]),
'post_link_url' => $option('post_link_url', 'Default', []),
'post_link_url_page' => $option('post_link_url_page', 'Default', ['post_id' => ['type' => 'select_post']]),
'post_featured_image' => $option('post_featured_image', 'Default', []),
'post_meta_key' => $option('post_meta_key', 'Standard', [
'before' => $text(),
'after' => $text(),
'select_meta_key' => ['type' => 'select'],
'meta_key' => $text(),
'enable_html' => ['type' => 'yes_no_button'],
]),
'loop_post_title' => $option('loop_post_title', 'Loop', [
'before' => $text(), 'after' => $text(), 'loop_position' => $text(),
]),
'loop_post_meta_key_manual_custom_field' => $option('loop_post_meta_key_manual_custom_field', 'Loop', [
'loop_meta_key' => $text(),
]),
// Odd entries a third-party filter could add.
'broken_fields' => $option('broken_fields', 'Default', ['a' => 'not-an-array', 'b' => ['type' => 3]]),
'no_fields' => ['id' => 'no_fields', 'type' => 'text'],
7 => $option('7', 'Default', []),
'not_an_array' => 'x',
];
}
public function testFlattensTheRegistryToOptionNameAndFieldTypes(): void
{
$map = DynamicContentMap::fromOptions(self::registry());
// 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(self::registry());
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(self::registry());
foreach (Placeholders::POST as $option) {
self::assertArrayHasKey($option, $map, $option);
foreach (array_keys(VariableRef::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(DesignSystemTest::sample(), $map);
foreach (array_keys(Placeholders::POST) as $key) {
$reference = $placeholders->resolve('{{post:' . $key . '}}');
self::assertSame(
$reference,
$placeholders->references()->canonical($reference, ComposeFixture::sanitizer()),
$key
);
}
}
public function testTheMapWorksInVariableRef(): void
{
$references = new VariableRef(DesignSystemTest::sample(), DynamicContentMap::fromOptions(self::registry()));
$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 DynamicContentMapTest::registry();
}))->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());
}
}
}