feat: Presets als Stapel, Gruppen-Presets und clear_overrides im Composer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 16:48:58 +02:00
co-authored by Claude Sonnet 5.5
parent fafa140546
commit be6ca5bee0
13 changed files with 549 additions and 74 deletions
@@ -5,12 +5,14 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Compose\Composer;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Integrations\Divi\Design\PresetBook;
use LIT\MCP\Tests\Support\ComposeFixture;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase;
@@ -424,6 +426,165 @@ final class ComposerTest extends TestCase
self::assertArrayNotHasKey('modulePreset', $result['attrs']);
}
/** Button presets p-1/p-2 as in the sample design system; p-2 carries a font color, g-1 is a font group preset. */
private static function coveringBook(): PresetBook
{
return PresetBook::fromDivi([
'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'],
]]],
]);
}
/** @return array<string, string> The option groups the tests give every button. */
private static function buttonGroups(string $module): array
{
return $module === 'divi/button' ? ['button.decoration.font.font' => 'divi/font'] : [];
}
public function testPresetStackIsStoredInTheGivenOrder(): void
{
$composer = ComposeFixture::composer();
$result = $composer->compose(
[['type' => 'button', 'text' => 'Los', 'preset' => ['CTA primär', 'p-1']]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $result['errors']);
self::assertSame(['p-2', 'p-1'], $result['blocks'][0]->attrs()['modulePreset']);
$update = $composer->update(Block::create('divi/button'), ['preset' => ['Standard', 'CTA primär']], 'changes[0]');
self::assertSame([], $update['errors']);
self::assertSame(['p-1', 'p-2'], $update['attrs']['modulePreset']);
}
public function testPresetStackErrorsKeepTheirInputPaths(): void
{
$composer = ComposeFixture::composer();
$button = Block::create('divi/button');
$unknown = $composer->update($button, ['preset' => ['Standard', 'Gibt es nicht']], 'changes[0]');
self::assertSame(
'unbekanntes Preset; vorhanden für divi/button: Standard, CTA primär',
$unknown['errors']['changes[0].preset.1']
);
self::assertArrayNotHasKey('modulePreset', $unknown['attrs']);
foreach ([[], ['Standard', 'p-1']] as $invalid) {
$result = $composer->update($button, ['preset' => $invalid], 'changes[0]');
self::assertNotSame([], preg_grep('/^changes\[0\]\.preset/', array_keys($result['errors'])));
self::assertArrayNotHasKey('modulePreset', $result['attrs']);
}
}
public function testGroupPresetsAreAssignedByShortNameAndRemovedWithNull(): void
{
$composer = ComposeFixture::composer(false, null, null, self::buttonGroups(...));
$groups = ['button.decoration.font.font' => ['groupName' => 'divi/font', 'presetIds' => ['g-1']]];
$update = $composer->update(Block::create('divi/button'), ['group_presets' => ['font' => 'Fließtext groß']], 'changes[0]');
self::assertSame([], $update['errors']);
self::assertSame($groups, NodeSummary::groupPresets(Block::create('divi/button', $update['attrs'])));
$created = $composer->compose(
[['type' => 'button', 'text' => 'Los', 'group_presets' => ['divi/font' => 'g-1']]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $created['errors']);
self::assertSame($groups, NodeSummary::groupPresets($created['blocks'][0]));
$removed = $composer->update(Block::create('divi/button', $update['attrs']), ['group_presets' => ['font' => null]], 'changes[0]');
self::assertSame([], $removed['errors']);
self::assertSame([], NodeSummary::groupPresets(Block::create('divi/button', $removed['attrs'])));
self::assertArrayNotHasKey('groupPreset', $removed['attrs']);
$unknown = $composer->update(Block::create('divi/button'), ['group_presets' => ['farbe' => 'x']], 'changes[0]');
self::assertArrayHasKey('changes[0].group_presets.farbe', $unknown['errors']);
}
/** Ruling R7: clear_overrides runs after set/attrs of the same node and removes only leaves a preset also sets. */
public function testClearOverridesRunsAfterSetAndAttrsOnANewNode(): void
{
$composer = ComposeFixture::composer(false, null, self::coveringBook());
$font = ['desktop' => ['color' => '#abcdef', 'family' => 'Open Sans']];
$kept = $composer->compose(
[['type' => 'button', 'text' => 'Los', 'preset' => 'CTA primär', 'attrs' => ['button.decoration.font.font' => $font]]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $kept['errors']);
self::assertSame(
['color' => '#abcdef', 'family' => 'Open Sans'],
AttrMap::desktop($kept['blocks'][0]->attrs(), 'button.decoration.font.font')
);
$cleared = $composer->compose(
[[
'type' => 'button',
'text' => 'Los',
'preset' => 'CTA primär',
'clear_overrides' => true,
'attrs' => ['button.decoration.font.font' => $font],
]],
Block::create('divi/column'),
'nodes'
);
self::assertSame([], $cleared['errors']);
$attrs = $cleared['blocks'][0]->attrs();
self::assertSame(['p-2'], $attrs['modulePreset']);
self::assertSame(['family' => 'Open Sans'], AttrMap::desktop($attrs, 'button.decoration.font.font'));
self::assertSame('Los', AttrMap::desktop($attrs, 'button.innerContent')['text']);
}
public function testClearOverridesOnAnUpdateKeepsEverythingNoPresetCovers(): void
{
$composer = ComposeFixture::composer(false, null, self::coveringBook());
$attrs = ['modulePreset' => ['p-2'], 'button' => ['decoration' => ['font' => ['font' => [
'desktop' => ['value' => ['color' => '#000000']],
]]]]];
$result = $composer->update(Block::create('divi/button', $attrs), ['clear_overrides' => true], 'changes[0]');
self::assertSame([], $result['errors']);
self::assertSame(['modulePreset' => ['p-2']], $result['attrs']);
$plain = Block::create('divi/button', ['button' => ['innerContent' => ['desktop' => ['value' => ['text' => 'Alt']]]]]);
$unchanged = $composer->update($plain, ['clear_overrides' => true], 'changes[0]');
self::assertSame([], $unchanged['errors']);
self::assertSame($plain->attrs(), $unchanged['attrs']);
$off = $composer->update(Block::create('divi/button', $attrs), ['clear_overrides' => false], 'changes[0]');
self::assertSame($attrs, $off['attrs']);
}
public function testClearOverridesMustBeABoolean(): void
{
$composer = ComposeFixture::composer();
$update = $composer->update(Block::create('divi/button'), ['clear_overrides' => 'ja'], 'changes[0]');
self::assertSame('true oder false erwartet', $update['errors']['changes[0].clear_overrides']);
$compose = $composer->compose(
[['type' => 'section', 'clear_overrides' => null, 'modules' => []]],
null,
'nodes'
);
self::assertSame('true oder false erwartet', $compose['errors']['nodes[0].clear_overrides']);
}
public function testBuilderVersionMustBeAVersion(): void
{
$this->expectException(InvalidArgumentException::class);
@@ -434,7 +595,7 @@ final class ComposerTest extends TestCase
ComposeFixture::shortform(),
ComposeFixture::attrWriter(),
ComposeFixture::cleaner(),
DiviFixtures::designSystem(),
ComposeFixture::presetAssignment(),
'5.2.1"},"x":{"'
);
}