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
@@ -8,10 +8,12 @@ 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\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;
@@ -26,7 +28,7 @@ final class DiviCompositionTest extends TestCase
/** @var list<int> */
private array $registryCalls = [];
private function composition(): DiviComposition
private function composition(?DesignSystemWriter $presets = null): DiviComposition
{
$modules = Mockery::mock(DiviModules::class);
$modules->allows('catalog')->andReturn(DiviFixtures::catalog());
@@ -45,10 +47,33 @@ final class DiviCompositionTest extends TestCase
ComposeFixture::sanitizer(),
ComposeFixture::media(),
$registry,
ComposeFixture::links()
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'],
]]],
'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' => '']);
@@ -156,4 +181,61 @@ final class DiviCompositionTest extends TestCase
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());
}
}
@@ -261,5 +261,51 @@ final class DiviUpdateAbilityTest extends DiviWriteAbilityTestCase
self::assertStringNotContainsString('1058', (string) json_encode($hidden));
self::assertStringContainsString('[global 1059]', (string) $shown['outline']);
}
}
public function testUpdateOffersPresetStacksGroupPresetsAndClearOverrides(): void
{
$change = $this->update()->inputSchema()['properties']['changes']['items']['properties'];
self::assertSame(['string', 'array', 'null'], $change['preset']['type']);
self::assertSame(['type' => 'string'], $change['preset']['items']);
self::assertSame([1, 10], [$change['preset']['minItems'], $change['preset']['maxItems']]);
self::assertSame('object', $change['group_presets']['type']);
self::assertSame(['string', 'array', 'null'], $change['group_presets']['additionalProperties']['type']);
self::assertSame([1, 10], [
$change['group_presets']['additionalProperties']['minItems'],
$change['group_presets']['additionalProperties']['maxItems'],
]);
self::assertSame('boolean', $change['clear_overrides']['type']);
$description = $this->update()->description();
foreach (['group_presets', 'clear_overrides', 'das letzte Preset'] as $text) {
self::assertStringContainsString($text, $description);
}
self::assertStringContainsString('group_presets', $this->insert()->description());
}
public function testClearOverridesMustBeABoolean(): void
{
$result = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [
['id' => 's0.r0.c1.m2', 'clear_overrides' => 'ja'],
]]);
self::assertToolError($result, 'validation_failed');
self::assertSame(
'true oder false erwartet',
self::clientError($result)['fields']['changes[0].clear_overrides']
);
self::assertSame([], $this->saved);
}
/** A pure clear_overrides that finds no covering value changes nothing and saves nothing. */
public function testClearOverridesWithoutAHitIsNoChange(): void
{
$result = $this->update()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'changes' => [
['id' => 's0.r0.c1.m2', 'clear_overrides' => true],
]]);
self::assertStringStartsWith('Keine Änderung', $result['summary']);
self::assertSame([], $this->saved);
}
}