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

312 lines
13 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 LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\Document;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
use LIT\MCP\Integrations\Divi\Compose\LoopScope;
use LIT\MCP\Integrations\Divi\Compose\StoredNodeGuard;
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class DiviCompositionTest extends TestCase
{
/** @var list<int> */
private array $registryCalls = [];
private function composition(?DesignSystemWriter $presets = null): DiviComposition
{
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(DiviFixtures::catalog());
$modules->allows('builderVersion')->andReturn('5.2.1');
$design = Mockery::mock(DesignSystemReader::class);
$design->allows('read')->andReturn(DiviFixtures::designSystem());
$registry = new DynamicContentMap(function (int $postId): array {
$this->registryCalls[] = $postId;
return DiviFixtures::dynamicContentRegistry();
});
return new DiviComposition(
$modules,
$design,
ComposeFixture::sanitizer(),
ComposeFixture::media(),
$registry,
ComposeFixture::links(),
presetSource: $presets
);
}
/** The preset source of the live wiring: Divi's stored presets (raw format) and the option groups per module. */
private function presetSource(int $reads = 1): DesignSystemWriter
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->expects('presets')->times($reads)->andReturn([
'module' => ['divi/button' => ['default' => 'p-1', 'items' => [
'p-1' => ['id' => 'p-1', 'name' => 'Standard', 'moduleName' => 'divi/button'],
'p-2' => [
'id' => 'p-2',
'name' => 'CTA primär',
'moduleName' => 'divi/button',
'attrs' => ['button' => ['decoration' => ['font' => ['font' => [
'desktop' => ['value' => ['color' => '#ffffff']],
]]]]],
],
]]],
'group' => ['divi/font' => ['default' => '', 'items' => [
'g-1' => ['id' => 'g-1', 'name' => 'Fließtext groß', 'groupName' => 'divi/font'],
]]],
]);
$writer->allows('groupsOf')->andReturnUsing(
static fn (string $module): array => $module === 'divi/button'
? ['button.decoration.font.font' => 'divi/font']
: []
);
return $writer;
}
private static function siteTitle(): string
{
return VariableRef::serialize('content', 'site_title', ['before' => '', 'after' => '']);
}
public function testOneLiveMapIsSharedByTheFullAndTheShortForm(): void
{
$composition = $this->composition()->forPost(7);
// Full form (AttrWriter).
$full = $composition->attrWriter()->apply(
'divi/text',
[],
['content.innerContent' => ['desktop' => self::siteTitle()]],
'attrs'
);
self::assertSame([], $full['errors']);
self::assertSame(self::siteTitle(), AttrMap::desktop($full['attrs'], 'content.innerContent'));
// Short form through the Composer (ShortformWriter) – same map, same AttrWriter.
$short = $composition->composer()->compose(
[['type' => 'button', 'text' => self::siteTitle()]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $short['errors']);
self::assertStringContainsString('site_title', json_encode($short['blocks'][0]->attrs(), JSON_THROW_ON_ERROR));
self::assertSame([7], $this->registryCalls, 'the registry is read once per post');
self::assertSame($composition->attrWriter(), $composition->attrWriter());
self::assertSame($composition->placeholders(), $composition->placeholders());
self::assertSame($composition->references(), $composition->references());
}
/** E13a: the composition hands its link targets to Placeholders; the result still passes ValueCleaner. */
public function testInternalLinksUseTheLinkTargets(): void
{
$composer = $this->composition()->forPost(7)->composer();
$column = Block::create('divi/column');
$page = $composer->compose([['type' => 'button', 'text' => 'Kontakt', 'url' => '{{link:839}}']], $column, 'nodes');
self::assertSame([], $page['errors']);
self::assertStringContainsString(
'post_link_url_page',
json_encode($page['blocks'][0]->attrs(), JSON_THROW_ON_ERROR)
);
$missing = $composer->compose([['type' => 'button', 'url' => '{{link:999}}']], $column, 'nodes');
self::assertStringContainsString('Eintrag 999', $missing['errors']['nodes[0].url']);
}
public function testWithoutAPostTheDefaultMapApplies(): void
{
$composition = $this->composition();
$result = $composition->attrWriter()->apply(
'divi/text',
[],
['content.innerContent' => ['desktop' => self::siteTitle()]],
'attrs'
);
self::assertStringContainsString('Unbekannter Name "site_title"', $result['errors']['attrs.content.innerContent']);
self::assertSame([], $this->registryCalls);
}
public function testForPostIsAFreshCompositionPerPost(): void
{
$base = $this->composition();
$seven = $base->forPost(7);
$eight = $base->forPost(8);
self::assertNotSame($base, $seven);
self::assertNotSame($seven->placeholders(), $eight->placeholders());
self::assertNotSame($base->placeholders(), $seven->placeholders());
self::assertNotSame($seven->references(), $eight->references());
self::assertNotSame($base->references(), $seven->references());
self::assertSame([7, 8], $this->registryCalls);
}
public function testRulesEditorAndWarnings(): void
{
$composition = $this->composition();
self::assertNull($composition->rules()->violation(Block::create('divi/column'), 'divi/button'));
self::assertNotNull($composition->rules()->violation(null, 'divi/button'));
self::assertSame([], $composition->warnings()->collect(NodeIndex::build(new Document()), []));
self::assertInstanceOf(TreeEditor::class, $composition->editor());
}
public function testStoredGuardSharesTheCleanerAndReferences(): void
{
$composition = DiviWriterFixture::composition(DiviWriterFixture::designReader())->forPost(7);
$code = Block::create('divi/code', ['builderVersion' => '5.2.1']);
self::assertInstanceOf(StoredNodeGuard::class, $composition->storedGuard());
self::assertSame(
[],
$composition->storedGuard()->problems(Block::create('divi/text', ['builderVersion' => '5.2.1']))
);
self::assertNotSame([], $composition->storedGuard()->problems($code), 'the editor sanitizer is used');
self::assertSame(
[],
DiviWriterFixture::composition(DiviWriterFixture::designReader(), true)->storedGuard()->problems($code)
);
}
public function testPresetStackAndGroupsComeFromTheWritersBookAndAreReadOnce(): void
{
$composer = $this->composition($this->presetSource())->forPost(7)->composer();
$result = $composer->compose([
['type' => 'button', 'text' => 'Eins', 'preset' => ['CTA primär', 'Standard'], 'group_presets' => ['font' => 'g-1']],
['type' => 'button', 'text' => 'Zwei', 'preset' => 'p-2'],
], Block::create('divi/column'), 'nodes');
self::assertSame([], $result['errors']);
self::assertSame(['p-2', 'p-1'], $result['blocks'][0]->attrs()['modulePreset']);
self::assertSame(['p-2'], $result['blocks'][1]->attrs()['modulePreset']);
self::assertSame(
['button.decoration.font.font' => ['groupName' => 'divi/font', 'presetIds' => ['g-1']]],
NodeSummary::groupPresets($result['blocks'][0])
);
}
public function testThePresetBookIsNotReadWithoutAPresetInTheInput(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->shouldNotReceive('presets');
$writer->shouldNotReceive('groupsOf');
$result = $this->composition($writer)->forPost(7)->composer()->compose(
[['type' => 'button', 'text' => 'Los']],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $result['errors']);
}
public function testWithoutAPresetSourceNoPresetExists(): void
{
$result = $this->composition()->forPost(7)->composer()->compose(
[['type' => 'button', 'text' => 'Los', 'preset' => 'Standard']],
Block::create('divi/column'),
'nodes'
);
self::assertSame(
'unbekanntes Preset; vorhanden für divi/button: (keine)',
$result['errors']['nodes[0].preset']
);
}
public function testOnePresetAssignmentPerBoundComposition(): void
{
$base = $this->composition($this->presetSource(0));
$seven = $base->forPost(7);
self::assertSame($seven->presets(), $seven->presets());
self::assertNotSame($seven->presets(), $base->forPost(8)->presets());
self::assertNotSame($base->presets(), $seven->presets());
}
public function testWarningsKnowTheWritersPresets(): void
{
$composition = $this->composition($this->presetSource())->forPost(7);
$button = Block::create('divi/button', [
'modulePreset' => ['p-2'],
'button' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['color' => '#000000']]]]]],
]);
$document = new Document();
$document->append($button);
$index = NodeIndex::build($document);
$warnings = $composition->warnings()->collect($index, [$button]);
$covering = array_values(
array_filter($warnings, static fn (string $w): bool => str_contains($w, 'überdecken'))
);
self::assertCount(1, $covering);
self::assertStringContainsString('„CTA primär“', $covering[0]);
}
/**
* Ruling X3: inLoop() gives a fresh composition bound to a loop scope – placeholders and references of the
* entry's map plus the loop options of that kind; outside a loop and in another kind loop content fails.
*/
public function testInLoopChecksLoopContentAgainstItsScope(): void
{
$post = $this->composition()->forPost(7);
$inPostLoop = $post->inLoop(LoopScope::ofQueryType('post_types'));
$compose = static fn (DiviComposition $composition, string $content = '{{loop:post_title}}'): array => $composition
->composer()->compose(
[['type' => 'text', 'content' => $content]],
Block::create('divi/column'),
'nodes'
);
$inside = $compose($inPostLoop);
self::assertSame([], $inside['errors']);
self::assertSame(
VariableRef::serialize('content', 'loop_post_title', ['before' => '', 'after' => '']),
AttrMap::desktop($inside['blocks'][0]->attrs(), 'content.innerContent')
);
// Placeholders get the loop fields: the short form carries before/after; outside a loop it is refused.
$date = $compose($inPostLoop, '{{loop:post_date}}');
self::assertSame([], $date['errors']);
self::assertStringContainsString(
'loop_post_date',
(string) AttrMap::desktop($date['blocks'][0]->attrs(), 'content.innerContent')
);
self::assertStringContainsString('braucht eine Divi-Schleife', implode(' ', $compose($post, '{{loop:post_date}}')['errors']));
self::assertStringContainsString('braucht eine Divi-Schleife', implode(' ', $compose($post)['errors']));
self::assertStringContainsString(
'passt nicht zur Schleife dieses Elements (Typ: Terme)',
implode(' ', $compose($post->inLoop(LoopScope::ofQueryType('post_taxonomies')))['errors'])
);
self::assertNotSame($post->references(), $inPostLoop->references());
self::assertNotSame($post->placeholders(), $inPostLoop->placeholders());
self::assertSame($inPostLoop->references(), $inPostLoop->references(), 'one VariableRef per scope');
}
}