Files

226 lines
10 KiB
PHP
Raw Permalink 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\Compose;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase;
final class PlaceholdersTest extends TestCase
{
/** Loop fields of a post loop as DynamicContentMap::loopFields() gives them (Divi 5.15.1 shapes). */
private const POST_LOOP_FIELDS = [
'loop_post_date' => [
'before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text',
'date_format' => 'select', 'custom_date_format' => 'text',
],
'loop_post_featured_image' => ['loop_position' => 'text', 'thumbnail_size' => 'select'],
];
/**
* Rows with a third value true go into a URL field (ruling P4: only the link row – URL mode
* refuses colour references by design).
*
* @return array<string, array{0: string, 1: string, 2?: bool}>
*/
public static function placeholders(): array
{
return [
'color alias' => ['{{color:primary}}', '$variable({"type":"color","value":{"name":"gcid-primary-color","settings":{}}})$'],
'color label' => ['{{ color : Nachtblau }}', '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$'],
'color id' => ['{{color:gcid-a1b2}}', '$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$'],
'variable' => ['{{var:Telefon}}', '$variable({"type":"content","value":{"name":"gvid-s1","settings":{}}})$'],
'variable id' => ['{{var:gvid-s1}}', '$variable({"type":"content","value":{"name":"gvid-s1","settings":{}}})$'],
'font' => ['{{font:heading}}', '$variable({"type":"content","value":{"name":"--et_global_heading_font","settings":{}}})$'],
'post title' => [
'{{post:title}}',
'$variable({"type":"content","value":{"name":"post_title","settings":{"before":"","after":""}}})$',
],
'featured image' => ['{{post:featured_image}}', '$variable({"type":"content","value":{"name":"post_featured_image","settings":{}}})$'],
'custom field' => [
'{{field:telefon_mobil}}',
'$variable({"type":"content","value":{"name":"custom_meta_telefon_mobil","settings":{"before":"","after":""}}})$',
],
'internal link' => [
'{{link:839}}',
'$variable({"type":"content","value":{"name":"post_link_url_page","settings":{"post_id":"839"}}})$',
true,
],
'plain text' => ['Hallo Welt', 'Hallo Welt'],
];
}
/** @dataProvider placeholders */
public function testResolvesToDiviVariables(string $input, string $expected, bool $url = false): void
{
$placeholders = ComposeFixture::placeholders();
$resolved = $placeholders->resolve($input);
self::assertSame($expected, $resolved);
if ($expected !== $input) {
// What placeholders produce passes the strict checker unchanged (ruling S1/S25); the live
// registry map knows post_link_url_page, the URL mode allows it.
$references = new VariableRef(
DiviFixtures::designSystem(),
DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry())
);
self::assertSame($resolved, $references->canonical($resolved, ComposeFixture::sanitizer(), $url));
}
}
/** @return array<string, array{string, string}> */
public static function invalid(): array
{
return [
'unknown color' => ['{{color:Lila}}', 'Vorhanden: primary, body, Primärfarbe, Fließtextfarbe, Nachtblau.'],
'unknown color id' => ['{{color:gcid-nope}}', 'Unbekannte globale Farbe'],
'unknown variable' => ['{{var:Fax}}', 'Vorhanden: Radius, Telefon'],
'unknown font' => ['{{font:mono}}', 'heading, body'],
'unknown post value' => ['{{post:views}}', 'title, excerpt'],
'bad field key' => ['{{field:a b}}', 'Ungültiger Feldschlüssel'],
'protected field key' => ['{{field:_edit_lock}}', 'Ungültiger Feldschlüssel'],
'embedded' => ['Seite: {{post:title}}', 'allein im Wert'],
'embedded loop value' => ['Am {{loop:post_date}}', 'post, field, link, loop'],
'unknown loop value' => ['{{loop:views}}', 'Unbekannter Schleifen-Inhalt "views". Vorhanden: post_title,'],
'loop value with its prefix' => ['{{loop:loop_post_date}}', 'ohne "loop_"'],
'loop meta key option' => ['{{loop:post_meta_key_manual_custom_field}}', 'nur in der Vollform'],
'link without id' => ['{{link:start}}', 'Ungültige Beitrags-ID'],
'link to a missing post' => ['{{link:999}}', 'Eintrag 999'],
'link without link targets' => ['{{link:0}}', 'Ungültige Beitrags-ID'],
'link with a leading zero' => ['{{link:0839}}', 'Ungültige Beitrags-ID'],
'link beyond PHP_INT_MAX' => ['{{link:9999999999999999999}}', 'Ungültige Beitrags-ID'],
];
}
/** @dataProvider invalid */
public function testRejectsUnknownNamesWithTheExistingOnes(string $input, string $message): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($message);
ComposeFixture::placeholders()->resolve($input);
}
/** @return array<string, array{string, string}> */
public static function ambiguous(): array
{
return [
'color label' => ['{{color:blau}}', 'gcid-b1, gcid-b2'],
'variable label' => ['{{var:Info}}', 'gvid-i1, gvid-i2'],
];
}
/**
* Ruling S13a: a label two entries share never silently picks the first one; IDs work directly.
*
* @dataProvider ambiguous
*/
public function testAmbiguousLabelsFailWithTheIds(string $input, string $ids): void
{
$placeholders = new Placeholders(new DesignSystem(
[
['id' => 'gcid-b1', 'label' => 'Blau', 'value' => '#00f'],
['id' => 'gcid-b2', 'label' => 'blau', 'value' => '#00a'],
],
[
['id' => 'gvid-i1', 'type' => 'strings', 'label' => 'Info', 'value' => 'a'],
['id' => 'gvid-i2', 'type' => 'strings', 'label' => 'Info', 'value' => 'b'],
],
[],
['heading' => '', 'body' => ''],
[]
));
try {
$placeholders->resolve($input);
self::fail('Ambiguous label accepted.');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString('mehrdeutig', $e->getMessage());
self::assertStringContainsString($ids, $e->getMessage());
}
self::assertStringContainsString('"gcid-b2"', $placeholders->resolve('{{color:gcid-b2}}'));
self::assertStringContainsString('"gvid-i2"', $placeholders->resolve('{{var:gvid-i2}}'));
}
public function testColorTypeVariablesBecomeColorReferences(): void
{
$placeholders = new Placeholders(new DesignSystem(
[],
[['id' => 'gvid-c1', 'type' => 'colors', 'label' => 'Akzent', 'value' => '#f00']],
[],
['heading' => '', 'body' => ''],
[]
));
self::assertSame(
'$variable({"type":"color","value":{"name":"gvid-c1","settings":{}}})$',
$placeholders->resolve('{{var:Akzent}}')
);
}
public function testResolvesDeepValues(): void
{
self::assertSame(
['color' => '$variable({"type":"color","value":{"name":"gcid-primary-color","settings":{}}})$', 'size' => '20px'],
ComposeFixture::placeholders()->resolveDeep(['color' => '{{color:primary}}', 'size' => '20px'])
);
}
public function testLinksNeedLinkTargets(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Eintrag 839');
(new Placeholders(DiviFixtures::designSystem()))->resolve('{{link:839}}');
}
/**
* Ruling X4: {{loop:<option>}} names the loop option without its prefix; before/after only where the loop
* fields of the composition's scope give the option those settings (Divi has none on image options).
*/
public function testLoopPlaceholdersNameTheLoopOption(): void
{
$placeholders = new Placeholders(DiviFixtures::designSystem(), null, self::POST_LOOP_FIELDS);
$date = $placeholders->resolve('{{ loop : Post_Date }}');
self::assertSame(
'$variable({"type":"content","value":{"name":"loop_post_date","settings":{"before":"","after":""}}})$',
$date
);
self::assertSame(
'$variable({"type":"content","value":{"name":"loop_post_featured_image","settings":{}}})$',
$placeholders->resolve('{{loop:post_featured_image}}')
);
// Not in the scope's fields (no loop, another kind): no settings – VariableRef names the loop problem.
self::assertSame(
'$variable({"type":"content","value":{"name":"loop_term_name","settings":{}}})$',
$placeholders->resolve('{{loop:term_name}}')
);
// What the placeholder writes passes the checker of a post loop unchanged.
$references = new VariableRef(
DiviFixtures::designSystem(),
DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry()) + self::POST_LOOP_FIELDS,
LoopScope::ofQueryType('post_types')
);
self::assertSame($date, $references->canonical($date, ComposeFixture::sanitizer()));
}
public function testEveryLoopPlaceholderNamesALoopOption(): void
{
foreach (Placeholders::LOOP as $short => $option) {
self::assertSame('loop_' . $short, $option);
self::assertStringNotContainsString('meta_key', $option, 'meta key options need settings (full form)');
}
}
}