feat: PresetScope und PresetBook – Preset-Bestand im Divi-Speicherformat

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-16 17:27:27 +02:00
co-authored by Claude Haiku 5.5
parent 36af846b3e
commit 578ccba118
4 changed files with 471 additions and 0 deletions
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Design\PresetBook;
use LIT\MCP\Integrations\Divi\Design\PresetScope;
use PHPUnit\Framework\TestCase;
use stdClass;
final class PresetBookTest extends TestCase
{
/** @return array<string, mixed> Stored format like GlobalPreset::get_data(). */
public static function stored(): array
{
return [
'module' => [
'divi/button' => ['default' => '', 'items' => [
'aaaaaaaaaa' => ['type' => 'module', 'id' => 'aaaaaaaaaa', 'name' => 'CTA primär',
'moduleName' => 'divi/button', 'attrs' => ['button' => ['x' => 1]]],
'bbbbbbbbbb' => ['type' => 'module', 'id' => 'bbbbbbbbbb', 'name' => 'Doppelt',
'moduleName' => 'divi/button', 'attrs' => []],
'cccccccccc' => ['type' => 'module', 'id' => 'cccccccccc', 'name' => 'doppelt',
'moduleName' => 'divi/button', 'attrs' => []],
]],
'divi/text' => ['default' => 'dddddddddd', 'items' => [
'dddddddddd' => ['type' => 'module', 'id' => 'dddddddddd', 'name' => 'Fließtext',
'moduleName' => 'divi/text', 'attrs' => [],
'groupPresets' => ['content.decoration.bodyFont' => [
'presetId' => ['gggggggggg'], 'groupName' => 'divi/font-body']]],
]],
],
'group' => [
'divi/font-body' => ['default' => '', 'items' => [
'gggggggggg' => ['type' => 'group', 'id' => 'gggggggggg', 'name' => 'Lesetext',
'groupName' => 'divi/font-body', 'groupId' => 'content.decoration.bodyFont',
'moduleName' => 'divi/text', 'attrs' => []],
]],
],
];
}
public function testFindsByIdOrUniqueName(): void
{
$book = PresetBook::fromDivi(self::stored());
$button = new PresetScope('module', 'divi/button');
self::assertSame('aaaaaaaaaa', $book->find($button, 'aaaaaaaaaa')['id']);
self::assertSame('aaaaaaaaaa', $book->find($button, 'cta PRIMÄR')['id']);
self::assertNull($book->find($button, 'Fehlt'));
self::assertNull($book->find(new PresetScope('module', 'divi/text'), 'aaaaaaaaaa'));
self::assertSame(['bbbbbbbbbb', 'cccccccccc'], $book->idsNamed($button, 'DOPPELT'));
}
public function testAmbiguousNameListsTheIds(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'Preset-Name "Doppelt" ist mehrdeutig; bitte die ID angeben: bbbbbbbbbb, cccccccccc.'
);
PresetBook::fromDivi(self::stored())->find(new PresetScope('module', 'divi/button'), 'Doppelt');
}
public function testStdClassGroupsBecomeArraysAndCountCoversBothKinds(): void
{
$raw = self::stored();
$raw['group'] = (object) ['divi/font-body' => (object) $raw['group']['divi/font-body']];
$book = PresetBook::fromDivi($raw);
self::assertSame(5, $book->count());
self::assertIsArray($book->data()['group']['divi/font-body']['items']);
self::assertSame('group', $book->scopeOf('gggggggggg')?->kind);
self::assertNull($book->scopeOf('zzzzzzzzzz'));
self::assertSame(0, PresetBook::fromDivi(new stdClass())->count());
}
public function testWithWithoutAndDefaultAreImmutable(): void
{
$book = PresetBook::fromDivi(self::stored());
$button = new PresetScope('module', 'divi/button');
$new = [
'type' => 'module', 'id' => 'eeeeeeeeee', 'name' => 'Neu', 'moduleName' => 'divi/button', 'attrs' => [],
];
$added = $book->with($button, $new)->withDefault($button, 'eeeeeeeeee');
$removed = $added->without($button, 'eeeeeeeeee');
self::assertSame(5, $book->count());
self::assertSame(6, $added->count());
self::assertSame('eeeeeeeeee', $added->defaultId($button));
self::assertSame('', $removed->defaultId($button), 'removing the default clears it');
self::assertNotSame($book->rev(), $added->rev());
self::assertSame($book->rev(), PresetBook::fromDivi(self::stored())->rev());
self::assertSame('', $book->defaultId(new PresetScope('group', 'divi/spacing')));
}
public function testNestedUsersOfAGroupPreset(): void
{
$users = PresetBook::fromDivi(self::stored())->nestedUsers('gggggggggg');
self::assertCount(1, $users);
self::assertSame(['dddddddddd', 'Fließtext'], [$users[0]['id'], $users[0]['name']]);
self::assertSame('divi/text', $users[0]['scope']->name);
}
public function testRestFormatListsItemsPerName(): void
{
$rest = PresetBook::fromDivi(self::stored())->toRest();
self::assertSame('dddddddddd', $rest['module'][1]['default']);
self::assertSame(['aaaaaaaaaa', 'bbbbbbbbbb', 'cccccccccc'], array_column($rest['module'][0]['items'], 'id'));
self::assertSame('gggggggggg', $rest['group'][0]['items'][0]['id']);
self::assertTrue(array_is_list($rest['module']));
}
public function testNewIdsLookLikeTheBuildersIds(): void
{
self::assertMatchesRegularExpression('/^[0-9a-z]{10}$/', PresetBook::newId());
self::assertNotSame(PresetBook::newId(), PresetBook::newId());
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Design\PresetScope;
use PHPUnit\Framework\TestCase;
final class PresetScopeTest extends TestCase
{
public function testShortAndFullNames(): void
{
self::assertSame('divi/button', PresetScope::fromInput(['module' => 'button'], 'items[0]')->name);
self::assertSame('module', PresetScope::fromInput(['module' => 'divi/button'], 'items[0]')->kind);
$group = PresetScope::fromInput(['group' => 'font-body'], 'items[0]');
self::assertSame(['group', 'divi/font-body'], [$group->kind, $group->name]);
self::assertSame('Gruppen-Preset divi/font-body', $group->label());
self::assertTrue($group->equals(new PresetScope('group', 'divi/font-body')));
}
/** @return array<string, array{array<string, mixed>, string}> */
public static function invalid(): array
{
return [
'nothing' => [[], 'items[0]'],
'both' => [['module' => 'button', 'group' => 'font'], 'items[0]'],
'not a name' => [['module' => 'Button 1'], 'items[0].module'],
'not a string' => [['group' => 5], 'items[0].group'],
'two slashes' => [['module' => 'divi/a/b'], 'items[0].module'],
];
}
/**
* @dataProvider invalid
* @param array<string, mixed> $item
*/
public function testInvalidInputNamesTheField(array $item, string $field): void
{
try {
PresetScope::fromInput($item, 'items[0]');
self::fail('expected an exception');
} catch (InvalidArgumentException $e) {
self::assertStringStartsWith($field . ':', $e->getMessage());
}
}
}