Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
342 lines
15 KiB
PHP
342 lines
15 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||
|
||
use Closure;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
final class PresetAssignmentTest extends TestCase
|
||
{
|
||
private const PATH = 'items[0]';
|
||
|
||
/** @var int How often the book closure ran. */
|
||
private int $bookCalls = 0;
|
||
|
||
/** Divi's stored format (formats §3): two module presets, group presets with two names the same ("Doppelt"). */
|
||
private static function stored(): array
|
||
{
|
||
$module = static fn (string $id, string $name, array $attrs = []): array => [
|
||
'type' => 'module', 'id' => $id, 'name' => $name, 'moduleName' => 'divi/blurb', 'attrs' => $attrs,
|
||
];
|
||
$group = static fn (string $id, string $name, string $groupName, string $groupId, array $attrs = []): array => [
|
||
'type' => 'group', 'id' => $id, 'name' => $name, 'groupName' => $groupName, 'groupId' => $groupId,
|
||
'moduleName' => 'divi/blurb', 'attrs' => $attrs,
|
||
];
|
||
|
||
return [
|
||
'module' => ['divi/blurb' => ['default' => '', 'items' => [
|
||
'bl00000001' => $module('bl00000001', 'Leistung', [
|
||
'title' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['weight' => '700']]]]]],
|
||
]),
|
||
'bl00000002' => $module('bl00000002', 'Karte'),
|
||
'bl00000003' => $module('bl00000003', 'Doppelt'),
|
||
'bl00000004' => $module('bl00000004', 'doppelt'),
|
||
'ba!d000005' => $module('ba!d000005', 'Kaputt'),
|
||
]]],
|
||
'group' => [
|
||
'divi/font-body' => ['default' => '', 'items' => [
|
||
'fb00000001' => $group('fb00000001', 'Fließtext', 'divi/font-body', 'content.decoration.bodyFont'),
|
||
'fb00000002' => $group('fb00000002', 'Fließtext groß', 'divi/font-body', 'content.decoration.bodyFont'),
|
||
'fb00000003' => $group('fb00000003', 'Doppelt', 'divi/font-body', 'content.decoration.bodyFont'),
|
||
'fb00000004' => $group('fb00000004', 'Doppelt', 'divi/font-body', 'content.decoration.bodyFont'),
|
||
]],
|
||
'divi/spacing' => ['default' => '', 'items' => [
|
||
'sp00000001' => $group('sp00000001', 'Abstand Karte', 'divi/spacing', 'module.decoration.spacing'),
|
||
]],
|
||
'divi/font' => ['default' => '', 'items' => [
|
||
'ft00000001' => $group('ft00000001', 'Titelschrift', 'divi/font', 'title.decoration.font'),
|
||
]],
|
||
],
|
||
];
|
||
}
|
||
|
||
private function assignment(?Closure $book = null): PresetAssignment
|
||
{
|
||
return new PresetAssignment(
|
||
$book ?? function (): PresetBook {
|
||
++$this->bookCalls;
|
||
|
||
return PresetBook::fromDivi(self::stored());
|
||
},
|
||
static fn (string $module): array => $module === 'divi/blurb' ? [
|
||
'title.decoration.font' => 'divi/font',
|
||
'content.decoration.bodyFont' => 'divi/font-body',
|
||
'module.decoration.spacing' => 'divi/spacing',
|
||
'button.decoration.spacing' => 'divi/spacing',
|
||
] : [],
|
||
static fn (string $id): bool => preg_match('/^[0-9a-z]+$/D', $id) === 1
|
||
);
|
||
}
|
||
|
||
public function testSingleNameOrIdBecomesAStackOfOne(): void
|
||
{
|
||
$byName = $this->assignment()->apply('divi/blurb', ['x' => 1], ['preset' => 'LEISTUNG'], self::PATH);
|
||
$byId = $this->assignment()->apply('divi/blurb', [], ['preset' => 'bl00000002'], self::PATH);
|
||
|
||
self::assertSame([], $byName['errors']);
|
||
self::assertSame(['x' => 1, 'modulePreset' => ['bl00000001']], $byName['attrs']);
|
||
self::assertSame(['modulePreset' => ['bl00000002']], $byId['attrs']);
|
||
}
|
||
|
||
public function testListKeepsTheOrderOfTheStack(): void
|
||
{
|
||
$result = $this->assignment()->apply(
|
||
'divi/blurb',
|
||
['modulePreset' => ['alt']],
|
||
['preset' => ['Karte', 'bl00000001']],
|
||
self::PATH
|
||
);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(['modulePreset' => ['bl00000002', 'bl00000001']], $result['attrs']);
|
||
}
|
||
|
||
public function testNullAndEmptyStringRemoveThePresetWithoutLoadingTheBook(): void
|
||
{
|
||
foreach ([null, ''] as $preset) {
|
||
$result = $this->assignment()->apply(
|
||
'divi/blurb',
|
||
['modulePreset' => ['bl00000001'], 'builderVersion' => '5.2.1'],
|
||
['preset' => $preset],
|
||
self::PATH
|
||
);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(['builderVersion' => '5.2.1'], $result['attrs']);
|
||
}
|
||
self::assertSame(0, $this->bookCalls);
|
||
}
|
||
|
||
public function testSpecWithoutPresetKeysChangesNothing(): void
|
||
{
|
||
$attrs = ['modulePreset' => ['bl00000001']];
|
||
$result = $this->assignment()->apply('divi/blurb', $attrs, ['label' => 'egal'], self::PATH);
|
||
|
||
self::assertSame(['attrs' => $attrs, 'errors' => []], $result);
|
||
self::assertSame(0, $this->bookCalls);
|
||
}
|
||
|
||
public function testStackErrorsLeaveTheStackUntouched(): void
|
||
{
|
||
$attrs = ['modulePreset' => ['alt']];
|
||
$assignment = $this->assignment();
|
||
|
||
$unknown = $assignment->apply('divi/blurb', $attrs, ['preset' => 'Gibt es nicht'], self::PATH);
|
||
self::assertSame(
|
||
['items[0].preset' => 'unbekanntes Preset; vorhanden für divi/blurb: Leistung, Karte, Doppelt, doppelt, Kaputt'],
|
||
$unknown['errors']
|
||
);
|
||
self::assertSame($attrs, $unknown['attrs']);
|
||
|
||
$ambiguous = $assignment->apply('divi/blurb', $attrs, ['preset' => ['Karte', 'Doppelt']], self::PATH);
|
||
self::assertSame(
|
||
['items[0].preset.1' => 'Preset-Name "Doppelt" ist mehrdeutig; bitte die ID angeben: '
|
||
. 'bl00000003, bl00000004.'],
|
||
$ambiguous['errors']
|
||
);
|
||
self::assertSame($attrs, $ambiguous['attrs']);
|
||
|
||
$duplicate = $assignment->apply('divi/blurb', $attrs, ['preset' => ['Karte', 'bl00000001', 'bl00000002']], self::PATH);
|
||
self::assertSame(['items[0].preset.2' => 'Preset "bl00000002" steht mehrfach im Stapel'], $duplicate['errors']);
|
||
|
||
$invalid = $assignment->apply('divi/blurb', $attrs, ['preset' => 'Kaputt'], self::PATH);
|
||
self::assertSame(['items[0].preset' => 'Preset-ID "ba!d000005" ist ungültig'], $invalid['errors']);
|
||
self::assertSame($attrs, $invalid['attrs']);
|
||
}
|
||
|
||
public function testStackShapeErrors(): void
|
||
{
|
||
$assignment = $this->assignment();
|
||
|
||
$empty = $assignment->apply('divi/blurb', [], ['preset' => []], self::PATH);
|
||
self::assertSame(['items[0].preset' => 'leere Liste – null entfernt das Preset'], $empty['errors']);
|
||
|
||
$tooMany = $assignment->apply('divi/blurb', [], ['preset' => array_fill(0, 11, 'Karte')], self::PATH);
|
||
self::assertSame(['items[0].preset' => 'höchstens 10 Presets im Stapel'], $tooMany['errors']);
|
||
|
||
$notText = $assignment->apply('divi/blurb', [], ['preset' => ['Karte', 5]], self::PATH);
|
||
self::assertSame(['items[0].preset.1' => 'Preset-Name oder -ID als Text erwartet'], $notText['errors']);
|
||
|
||
$object = $assignment->apply('divi/blurb', [], ['preset' => ['a' => 'Karte']], self::PATH);
|
||
self::assertSame(['items[0].preset' => 'Text, Liste von Texten oder null erwartet'], $object['errors']);
|
||
|
||
$number = $assignment->apply('divi/blurb', [], ['preset' => 7], self::PATH);
|
||
self::assertSame(['items[0].preset' => 'Text, Liste von Texten oder null erwartet'], $number['errors']);
|
||
}
|
||
|
||
public function testGroupPresetsByShortNameFullNameAndPath(): void
|
||
{
|
||
$result = $this->assignment()->apply('divi/blurb', ['builderVersion' => '5.2.1'], ['group_presets' => [
|
||
'font-body' => 'Fließtext',
|
||
'divi/font' => 'ft00000001',
|
||
'button.decoration.spacing' => ['Abstand Karte'],
|
||
]], self::PATH);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(
|
||
[
|
||
'builderVersion' => '5.2.1',
|
||
'groupPreset' => [
|
||
'content.decoration.bodyFont' => ['presetId' => ['fb00000001'], 'groupName' => 'divi/font-body'],
|
||
'title.decoration.font' => ['presetId' => ['ft00000001'], 'groupName' => 'divi/font'],
|
||
'button.decoration.spacing' => ['presetId' => ['sp00000001'], 'groupName' => 'divi/spacing'],
|
||
],
|
||
],
|
||
$result['attrs']
|
||
);
|
||
}
|
||
|
||
public function testGroupPresetListIsAStackAndOtherGroupsStay(): void
|
||
{
|
||
$attrs = ['groupPreset' => [
|
||
'title.decoration.font' => ['presetId' => ['ft00000001'], 'groupName' => 'divi/font'],
|
||
]];
|
||
|
||
$result = $this->assignment()->apply(
|
||
'divi/blurb',
|
||
$attrs,
|
||
['group_presets' => ['font-body' => ['Fließtext', 'Fließtext groß']]],
|
||
self::PATH
|
||
);
|
||
|
||
self::assertSame([], $result['errors']);
|
||
self::assertSame(['fb00000001', 'fb00000002'], $result['attrs']['groupPreset']['content.decoration.bodyFont']['presetId']);
|
||
self::assertSame($attrs['groupPreset']['title.decoration.font'], $result['attrs']['groupPreset']['title.decoration.font']);
|
||
}
|
||
|
||
public function testNullRemovesTheGroupEntryAndAnEmptyGroupPresetDisappears(): void
|
||
{
|
||
$attrs = ['groupPreset' => [
|
||
'title.decoration.font' => ['presetId' => ['ft00000001'], 'groupName' => 'divi/font'],
|
||
'content.decoration.bodyFont' => ['presetId' => ['fb00000001'], 'groupName' => 'divi/font-body'],
|
||
]];
|
||
|
||
$one = $this->assignment()->apply('divi/blurb', $attrs, ['group_presets' => ['font' => null]], self::PATH);
|
||
self::assertSame(['content.decoration.bodyFont'], array_keys($one['attrs']['groupPreset']));
|
||
|
||
$both = $this->assignment()->apply(
|
||
'divi/blurb',
|
||
$attrs,
|
||
['group_presets' => ['font' => null, 'font-body' => null]],
|
||
self::PATH
|
||
);
|
||
self::assertSame([], $both['errors']);
|
||
self::assertSame([], $both['attrs']);
|
||
}
|
||
|
||
public function testGroupKeyErrors(): void
|
||
{
|
||
$assignment = $this->assignment();
|
||
|
||
$unknown = $assignment->apply('divi/blurb', [], ['group_presets' => ['sizing' => 'X']], self::PATH);
|
||
self::assertSame(
|
||
['items[0].group_presets.sizing' => 'keine Gruppe "sizing" in divi/blurb; vorhanden: font, font-body, spacing'],
|
||
$unknown['errors']
|
||
);
|
||
|
||
$ambiguous = $assignment->apply('divi/blurb', [], ['group_presets' => ['spacing' => 'Abstand Karte']], self::PATH);
|
||
self::assertSame(
|
||
['items[0].group_presets.spacing' => 'Gruppe "spacing" ist mehrdeutig; bitte den Attributpfad angeben: '
|
||
. 'module.decoration.spacing, button.decoration.spacing.'],
|
||
$ambiguous['errors']
|
||
);
|
||
|
||
$missing = $assignment->apply('divi/divider', [], ['group_presets' => ['font-body' => 'Fließtext']], self::PATH);
|
||
self::assertSame(
|
||
['items[0].group_presets.font-body' => 'keine Gruppe "font-body" in divi/divider; vorhanden: (keine)'],
|
||
$missing['errors']
|
||
);
|
||
}
|
||
|
||
public function testGroupPresetValueErrors(): void
|
||
{
|
||
$assignment = $this->assignment();
|
||
|
||
$result = $assignment->apply('divi/blurb', [], ['group_presets' => [
|
||
'font-body' => 'Gibt es nicht',
|
||
'font' => 7,
|
||
'module.decoration.spacing' => [],
|
||
]], self::PATH);
|
||
self::assertSame(
|
||
[
|
||
'items[0].group_presets.font-body' => 'unbekanntes Gruppen-Preset "Gibt es nicht" (divi/font-body)',
|
||
'items[0].group_presets.font' => 'Text, Liste von Texten oder null erwartet',
|
||
'items[0].group_presets.module.decoration.spacing' => 'leere Liste – null entfernt das Gruppen-Preset',
|
||
],
|
||
$result['errors']
|
||
);
|
||
self::assertSame([], $result['attrs']);
|
||
|
||
$ambiguous = $assignment->apply('divi/blurb', [], ['group_presets' => ['font-body' => 'Doppelt']], self::PATH);
|
||
self::assertSame(
|
||
['items[0].group_presets.font-body' => 'Preset-Name "Doppelt" ist mehrdeutig; bitte die ID angeben: '
|
||
. 'fb00000003, fb00000004.'],
|
||
$ambiguous['errors']
|
||
);
|
||
|
||
$duplicate = $assignment->apply(
|
||
'divi/blurb',
|
||
[],
|
||
['group_presets' => ['font-body' => ['Fließtext', 'fb00000001']]],
|
||
self::PATH
|
||
);
|
||
self::assertSame(
|
||
['items[0].group_presets.font-body' => 'Preset "fb00000001" steht mehrfach im Stapel'],
|
||
$duplicate['errors']
|
||
);
|
||
|
||
$shape = $assignment->apply('divi/blurb', [], ['group_presets' => 'font-body'], self::PATH);
|
||
self::assertSame(['items[0].group_presets' => 'Objekt Gruppe → Preset erwartet'], $shape['errors']);
|
||
}
|
||
|
||
public function testModulePresetFromAnotherModuleIsUnknown(): void
|
||
{
|
||
$result = $this->assignment()->apply('divi/text', [], ['preset' => 'Leistung'], self::PATH);
|
||
|
||
self::assertSame(['items[0].preset' => 'unbekanntes Preset; vorhanden für divi/text: (keine)'], $result['errors']);
|
||
}
|
||
|
||
public function testBookIsLoadedLazilyAndAtMostOnce(): void
|
||
{
|
||
$assignment = $this->assignment();
|
||
self::assertSame(0, $this->bookCalls);
|
||
|
||
$assignment->apply('divi/blurb', [], ['preset' => 'Karte', 'group_presets' => ['font' => 'Titelschrift']], self::PATH);
|
||
$assignment->names();
|
||
$assignment->clearOverrides(['modulePreset' => ['bl00000001']]);
|
||
$assignment->overrides()->find(['modulePreset' => ['bl00000001']]);
|
||
|
||
self::assertSame(1, $this->bookCalls);
|
||
}
|
||
|
||
public function testNamesListsEveryPreset(): void
|
||
{
|
||
$names = $this->assignment()->names();
|
||
|
||
self::assertSame('Leistung', $names['bl00000001']);
|
||
self::assertSame('Fließtext groß', $names['fb00000002']);
|
||
self::assertSame('Titelschrift', $names['ft00000001']);
|
||
self::assertCount(11, $names);
|
||
}
|
||
|
||
public function testClearOverridesAndOverridesShareTheSameBook(): void
|
||
{
|
||
$assignment = $this->assignment();
|
||
$local = [
|
||
'modulePreset' => ['bl00000001'],
|
||
'title' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['weight' => '400']]]]]],
|
||
];
|
||
|
||
self::assertSame(
|
||
['bl00000001' => ['title.decoration.font.font|desktop|value|weight']],
|
||
$assignment->overrides()->find($local)
|
||
);
|
||
self::assertSame($assignment->overrides(), $assignment->overrides());
|
||
self::assertSame(['modulePreset' => ['bl00000001']], $assignment->clearOverrides($local));
|
||
}
|
||
}
|