Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
248 lines
11 KiB
PHP
248 lines
11 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||
use LIT\MCP\Integrations\Divi\Compose\StoredNodeGuard;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetScope;
|
||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
final class PresetComposerTest extends TestCase
|
||
{
|
||
private const NOW = 1760000000000;
|
||
|
||
/** Divi's mapping in small: decoration paths are preset attributes, innerContent is content. */
|
||
private static function presetAttrs(): \Closure
|
||
{
|
||
return static function (string $module, array $attrs): array {
|
||
$all = [];
|
||
foreach ($attrs as $key => $value) {
|
||
if (is_array($value)) {
|
||
$kept = array_diff_key($value, ['innerContent' => true]);
|
||
if ($kept !== []) {
|
||
$all[$key] = $kept;
|
||
}
|
||
}
|
||
}
|
||
|
||
return ['all' => $all, 'style' => $all, 'render' => []];
|
||
};
|
||
}
|
||
|
||
private static function composer(bool $unfiltered = true, ?\Closure $groupsOf = null): PresetComposer
|
||
{
|
||
return new PresetComposer(
|
||
ComposeFixture::composer($unfiltered),
|
||
new StoredNodeGuard(
|
||
ComposeFixture::cleaner($unfiltered),
|
||
ComposeFixture::references(),
|
||
ComposeFixture::sanitizer($unfiltered)
|
||
),
|
||
self::presetAttrs(),
|
||
$groupsOf ?? static fn (string $module): array => [
|
||
'module.decoration.spacing' => 'divi/spacing',
|
||
'button.decoration.font' => 'divi/font',
|
||
],
|
||
'5.15.1',
|
||
static fn (): int => self::NOW
|
||
);
|
||
}
|
||
|
||
private static function book(): PresetBook
|
||
{
|
||
return PresetBook::fromDivi(PresetBookTest::stored());
|
||
}
|
||
|
||
public function testNewModulePresetFromAttrs(): void
|
||
{
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'CTA sekundär',
|
||
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#123456']]],
|
||
], 'items[0]', null);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
$item = $result['item'];
|
||
self::assertMatchesRegularExpression('/^[0-9a-z]{10}$/', $item['id']);
|
||
self::assertSame(['module', 'CTA sekundär', 'divi/button', '5.15.1'], [$item['type'], $item['name'],
|
||
$item['moduleName'], $item['version']]);
|
||
self::assertSame([self::NOW, self::NOW], [$item['created'], $item['updated']]);
|
||
self::assertSame('#123456', $item['attrs']['button']['decoration']['background']['desktop']['value']['color']);
|
||
self::assertSame($item['attrs'], $item['styleAttrs']);
|
||
self::assertArrayNotHasKey('renderAttrs', $item);
|
||
}
|
||
|
||
public function testContentPathsAreRefused(): void
|
||
{
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'Mit Text',
|
||
'set' => ['text' => 'Jetzt buchen'],
|
||
], 'items[0]', null);
|
||
|
||
self::assertArrayHasKey('items[0].attrs.button.innerContent', $result['errors']);
|
||
self::assertStringContainsString('kein Preset-Attribut', $result['errors']['items[0].attrs.button.innerContent']);
|
||
}
|
||
|
||
public function testChangingMergesKeepsIdCreatedAndOrder(): void
|
||
{
|
||
$existing = self::book()->items(new PresetScope('module', 'divi/button'))['aaaaaaaaaa'];
|
||
$existing['created'] = 5;
|
||
$existing['order'] = 3;
|
||
$existing['attrs'] = ['button' => ['decoration' => ['border' => ['desktop' => ['value' => ['radius' => '4px']]]]]];
|
||
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), $existing, [
|
||
'name' => 'CTA Primär',
|
||
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#000000']]],
|
||
], 'items[0]', null);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(['aaaaaaaaaa', 'CTA Primär', 5, self::NOW, 3], [$result['item']['id'], $result['item']['name'],
|
||
$result['item']['created'], $result['item']['updated'], $result['item']['order']]);
|
||
self::assertSame('4px', $result['item']['attrs']['button']['decoration']['border']['desktop']['value']['radius']);
|
||
}
|
||
|
||
/** Final review M-5: the ability resolves name/rename; the composer only takes "name" as given. */
|
||
public function testOnlyTheResolvedNameCounts(): void
|
||
{
|
||
$existing = self::book()->items(new PresetScope('module', 'divi/button'))['aaaaaaaaaa'];
|
||
|
||
$kept = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), $existing, [
|
||
'rename' => 'Ignoriert',
|
||
], 'items[0]', null);
|
||
$missing = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'rename' => 'Ignoriert',
|
||
], 'items[0]', null);
|
||
|
||
self::assertSame('CTA primär', $kept['item']['name']);
|
||
self::assertSame('Name des neuen Presets fehlt', $missing['errors']['items[0].name']);
|
||
}
|
||
|
||
public function testFromNodeTakesTheDesignButNotTheContent(): void
|
||
{
|
||
$node = Block::create('divi/button', [
|
||
'button' => [
|
||
'innerContent' => ['desktop' => ['value' => ['text' => 'Hallo']]],
|
||
'decoration' => ['background' => ['desktop' => ['value' => ['color' => '#abcdef']]]],
|
||
],
|
||
]);
|
||
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'Aus Seite',
|
||
], 'items[0]', $node);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame('#abcdef', $result['item']['attrs']['button']['decoration']['background']['desktop']['value']['color']);
|
||
self::assertArrayNotHasKey('innerContent', $result['item']['attrs']['button']);
|
||
}
|
||
|
||
public function testEditorCannotCarryBuilderOnlyValuesFromANode(): void
|
||
{
|
||
$node = Block::create('divi/button', [
|
||
'css' => ['desktop' => ['value' => ['mainElement' => 'color:red']]],
|
||
'button' => ['decoration' => ['background' => ['desktop' => ['value' => ['color' => '#000000']]]]],
|
||
]);
|
||
|
||
$result = self::composer(false)->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'Mit CSS',
|
||
], 'items[0]', $node);
|
||
|
||
self::assertArrayHasKey('items[0].from_node', $result['errors']);
|
||
}
|
||
|
||
public function testGroupPresetOnlyTakesItsGroup(): void
|
||
{
|
||
$result = self::composer()->compose(self::book(), new PresetScope('group', 'divi/spacing'), null, [
|
||
'name' => 'Abstand groß',
|
||
'source_module' => 'button',
|
||
'attrs' => [
|
||
'module.decoration.spacing' => ['desktop' => ['padding' => ['top' => '40px']]],
|
||
'button.decoration.background' => ['desktop' => ['color' => '#000000']],
|
||
],
|
||
], 'items[0]', null);
|
||
|
||
self::assertSame(
|
||
'gehört nicht zur Gruppe module.decoration.spacing',
|
||
$result['errors']['items[0].attrs.button.decoration.background']
|
||
);
|
||
self::assertSame(['group', 'divi/spacing', 'module.decoration.spacing', 'divi/button'], [
|
||
$result['item']['type'], $result['item']['groupName'], $result['item']['groupId'],
|
||
$result['item']['moduleName'],
|
||
]);
|
||
}
|
||
|
||
public function testNestedGroupPresetsByShortName(): void
|
||
{
|
||
$book = self::book()->with(new PresetScope('group', 'divi/font'), [
|
||
'type' => 'group', 'id' => 'ffffffffff', 'name' => 'Button-Schrift', 'groupName' => 'divi/font',
|
||
'groupId' => 'button.decoration.font', 'moduleName' => 'divi/button', 'attrs' => [],
|
||
]);
|
||
|
||
$result = self::composer()->compose($book, new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'Mit Schrift',
|
||
'group_presets' => ['font' => 'Button-Schrift'],
|
||
], 'items[0]', null);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(
|
||
['button.decoration.font' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font']],
|
||
$result['item']['groupPresets']
|
||
);
|
||
}
|
||
|
||
public function testUnknownGroupAndPresetAreErrors(): void
|
||
{
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [
|
||
'name' => 'X',
|
||
'group_presets' => ['sizing' => 'Gibt es nicht', 'font' => 'Fehlt'],
|
||
], 'items[0]', null);
|
||
|
||
self::assertStringContainsString('keine Gruppe', $result['errors']['items[0].group_presets.sizing']);
|
||
self::assertStringContainsString('unbekanntes Gruppen-Preset', $result['errors']['items[0].group_presets.font']);
|
||
}
|
||
|
||
/** Ruling R3: GroupKeys replaces the first-match search – a name two groups share needs the path. */
|
||
public function testAmbiguousGroupNameIsAnErrorAndThePathResolvesIt(): void
|
||
{
|
||
$groupsOf = static fn (string $module): array => [
|
||
'button.decoration.font' => 'divi/font',
|
||
'title.decoration.font' => 'divi/font',
|
||
];
|
||
$book = self::book()->with(new PresetScope('group', 'divi/font'), [
|
||
'type' => 'group', 'id' => 'ffffffffff', 'name' => 'Schrift', 'groupName' => 'divi/font',
|
||
'groupId' => 'title.decoration.font', 'moduleName' => 'divi/button', 'attrs' => [],
|
||
]);
|
||
$composer = self::composer(true, $groupsOf);
|
||
$scope = new PresetScope('module', 'divi/button');
|
||
|
||
$ambiguous = $composer->compose($book, $scope, null, [
|
||
'name' => 'X', 'group_presets' => ['font' => 'Schrift'],
|
||
], 'items[0]', null);
|
||
self::assertSame(
|
||
'Gruppe "font" ist mehrdeutig; bitte den Attributpfad angeben: '
|
||
. 'button.decoration.font, title.decoration.font.',
|
||
$ambiguous['errors']['items[0].group_presets.font']
|
||
);
|
||
|
||
$byPath = $composer->compose($book, $scope, null, [
|
||
'name' => 'X', 'group_presets' => ['title.decoration.font' => 'Schrift'],
|
||
], 'items[0]', null);
|
||
self::assertSame([], $byPath['errors']);
|
||
self::assertSame(
|
||
['title.decoration.font' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font']],
|
||
$byPath['item']['groupPresets']
|
||
);
|
||
}
|
||
|
||
public function testNewPresetNeedsAName(): void
|
||
{
|
||
$result = self::composer()->compose(self::book(), new PresetScope('module', 'divi/button'), null, [],
|
||
'items[0]', null);
|
||
|
||
self::assertSame('Name des neuen Presets fehlt', $result['errors']['items[0].name']);
|
||
}
|
||
}
|