Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
268 lines
12 KiB
PHP
268 lines
12 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||
|
||
use LIT\MCP\Integrations\Divi\Design\PresetOverrides;
|
||
use PHPUnit\Framework\TestCase;
|
||
use stdClass;
|
||
|
||
final class PresetOverridesTest extends TestCase
|
||
{
|
||
private const SPACING = 'module.decoration.spacing';
|
||
private const BODY_FONT = 'content.decoration.bodyFont';
|
||
|
||
/** A breakpoint map with one desktop value, the shape Divi stores at an attribute path. */
|
||
private static function desktop(mixed $value): array
|
||
{
|
||
return ['desktop' => ['value' => $value]];
|
||
}
|
||
|
||
/** Presets in Divi's stored format (formats §3): a module preset nesting a group preset, a group preset. */
|
||
private static function overrides(): PresetOverrides
|
||
{
|
||
$items = [
|
||
'mmmmmmmmmm' => [
|
||
'type' => 'module', 'id' => 'mmmmmmmmmm', 'name' => 'Blurb Leistung', 'moduleName' => 'divi/blurb',
|
||
'attrs' => [
|
||
'module' => ['decoration' => ['spacing' => self::desktop([
|
||
'padding' => ['top' => '20px', 'bottom' => '10px'],
|
||
])]],
|
||
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => '700'])]]],
|
||
],
|
||
'groupPresets' => [
|
||
'title.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body'],
|
||
],
|
||
],
|
||
'ssssssssss' => [
|
||
'type' => 'group', 'id' => 'ssssssssss', 'name' => 'Abstand Karte', 'groupName' => 'divi/spacing',
|
||
'groupId' => self::SPACING, 'moduleName' => 'divi/blurb',
|
||
'attrs' => ['module' => ['decoration' => ['spacing' => self::desktop([
|
||
'margin' => ['bottom' => '24px'],
|
||
])]]],
|
||
],
|
||
'ffffffffff' => [
|
||
'type' => 'group', 'id' => 'ffffffffff', 'name' => 'Fließtext', 'groupName' => 'divi/font-body',
|
||
'groupId' => self::BODY_FONT, 'moduleName' => 'divi/text',
|
||
'attrs' => ['content' => ['decoration' => ['bodyFont' => ['desktop' => [
|
||
'value' => ['size' => '18px'],
|
||
'hover' => ['size' => '20px'],
|
||
], 'phone' => ['value' => ['size' => '16px']]]]]],
|
||
],
|
||
];
|
||
|
||
return new PresetOverrides(static fn (string $id): ?array => $items[$id] ?? null);
|
||
}
|
||
|
||
public function testLeavesFlattenBreakpointsStatesAndSubpaths(): void
|
||
{
|
||
$leaves = PresetOverrides::leaves([
|
||
'builderVersion' => '5.2.1',
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'groupPreset' => [self::SPACING => ['presetId' => ['ssssssssss'], 'groupName' => 'divi/spacing']],
|
||
'module' => ['decoration' => ['spacing' => [
|
||
'desktop' => ['value' => ['padding' => ['top' => '20px'], 'syncVertical' => 'on'], 'hover' => [
|
||
'margin' => ['bottom' => '4px'],
|
||
]],
|
||
'phone' => ['value' => ['padding' => ['top' => '8px']]],
|
||
]]],
|
||
'title' => ['innerContent' => self::desktop('Hallo'), 'decoration' => ['font' => ['font' => self::desktop(
|
||
new stdClass()
|
||
)]]],
|
||
'content' => ['decoration' => ['bodyFont' => self::desktop(['families' => ['Lato', 'Arial']])]],
|
||
]);
|
||
|
||
self::assertSame(
|
||
[
|
||
'content.decoration.bodyFont|desktop|value|families' => ['Lato', 'Arial'],
|
||
'module.decoration.spacing|desktop|value|padding.top' => '20px',
|
||
'module.decoration.spacing|desktop|value|syncVertical' => 'on',
|
||
'module.decoration.spacing|desktop|hover|margin.bottom' => '4px',
|
||
'module.decoration.spacing|phone|value|padding.top' => '8px',
|
||
'title.innerContent|desktop|value|' => 'Hallo',
|
||
],
|
||
$leaves
|
||
);
|
||
}
|
||
|
||
public function testFindReportsOnlyDifferingValuesOfTheModulePreset(): void
|
||
{
|
||
$attrs = [
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px', 'bottom' => '10px']])]],
|
||
// 700 as a number is the same value as "700" (the builder writes strings, an API client may not).
|
||
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => 700])]]],
|
||
];
|
||
|
||
self::assertSame(
|
||
['mmmmmmmmmm' => ['module.decoration.spacing|desktop|value|padding.top']],
|
||
self::overrides()->find($attrs)
|
||
);
|
||
}
|
||
|
||
public function testNoOverlapNoReport(): void
|
||
{
|
||
// A local padding.left does not cover the preset's padding.top/bottom (array_replace_recursive).
|
||
$attrs = [
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['left' => '5px']])]],
|
||
'title' => ['innerContent' => self::desktop('Text')],
|
||
];
|
||
|
||
self::assertSame([], self::overrides()->find($attrs));
|
||
self::assertSame([], self::overrides()->find([]));
|
||
}
|
||
|
||
public function testGroupPresetLeavesMoveFromTheStoredGroupIdToTheBlocksKey(): void
|
||
{
|
||
// The preset was made for content.decoration.bodyFont; this block uses it on text.decoration.bodyFont.
|
||
$attrs = [
|
||
'groupPreset' => ['text.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body']],
|
||
'text' => ['decoration' => ['bodyFont' => [
|
||
'desktop' => ['value' => ['size' => '22px'], 'hover' => ['size' => '20px']],
|
||
'phone' => ['value' => ['size' => '12px']],
|
||
]]],
|
||
// Same path as the preset's stored groupId, but not the block's key: no overlap.
|
||
'content' => ['decoration' => ['bodyFont' => self::desktop(['size' => '99px'])]],
|
||
];
|
||
|
||
self::assertSame(
|
||
['ffffffffff' => [
|
||
'text.decoration.bodyFont|desktop|value|size',
|
||
'text.decoration.bodyFont|phone|value|size',
|
||
]],
|
||
self::overrides()->find($attrs)
|
||
);
|
||
}
|
||
|
||
public function testNestedGroupPresetsOfAnAssignedModulePresetCount(): void
|
||
{
|
||
$attrs = [
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'title' => ['decoration' => ['bodyFont' => self::desktop(['size' => '30px'])]],
|
||
];
|
||
|
||
// The nested entry sits on title.decoration.bodyFont of the module preset; its leaves move there.
|
||
self::assertSame(
|
||
['ffffffffff' => ['title.decoration.bodyFont|desktop|value|size']],
|
||
self::overrides()->find($attrs)
|
||
);
|
||
}
|
||
|
||
public function testPresetsNobodyAssignedAreNotCounted(): void
|
||
{
|
||
// Default presets ("", "default", "_initial") and unknown IDs are no explicit assignment (ruling R4).
|
||
$attrs = [
|
||
'modulePreset' => ['', 'default', '_initial', 'unbekannt'],
|
||
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px']])]],
|
||
];
|
||
|
||
self::assertSame([], self::overrides()->find($attrs));
|
||
self::assertSame($attrs, self::overrides()->clear($attrs));
|
||
}
|
||
|
||
public function testStackAndSingleStringAreRead(): void
|
||
{
|
||
$attrs = [
|
||
'modulePreset' => 'mmmmmmmmmm',
|
||
'groupPreset' => [self::SPACING => ['presetId' => 'ssssssssss', 'groupName' => 'divi/spacing']],
|
||
'module' => ['decoration' => ['spacing' => self::desktop([
|
||
'padding' => ['bottom' => '11px'], 'margin' => ['bottom' => '1px'],
|
||
])]],
|
||
];
|
||
|
||
self::assertSame(
|
||
[
|
||
'mmmmmmmmmm' => ['module.decoration.spacing|desktop|value|padding.bottom'],
|
||
'ssssssssss' => ['module.decoration.spacing|desktop|value|margin.bottom'],
|
||
],
|
||
self::overrides()->find($attrs)
|
||
);
|
||
}
|
||
|
||
public function testClearRemovesLocalLeavesOnPresetLeavesAndEmptyParents(): void
|
||
{
|
||
$attrs = [
|
||
'builderVersion' => '5.2.1',
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'module' => ['decoration' => ['spacing' => self::desktop([
|
||
// padding.top is a preset leaf (different value), padding.bottom too (same value), left is not.
|
||
'padding' => ['top' => '40px', 'bottom' => '10px', 'left' => '5px'],
|
||
])]],
|
||
'title' => [
|
||
'innerContent' => self::desktop('Hallo'),
|
||
'decoration' => ['font' => ['font' => self::desktop(['weight' => '400'])]],
|
||
],
|
||
];
|
||
|
||
self::assertSame(
|
||
[
|
||
'builderVersion' => '5.2.1',
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['left' => '5px']])]],
|
||
'title' => ['innerContent' => self::desktop('Hallo')],
|
||
],
|
||
self::overrides()->clear($attrs)
|
||
);
|
||
}
|
||
|
||
public function testClearDropsAWholeBranchWhenNothingElseIsLeft(): void
|
||
{
|
||
$attrs = [
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'title' => ['decoration' => ['font' => ['font' => self::desktop(['weight' => '400'])]]],
|
||
];
|
||
|
||
self::assertSame(['modulePreset' => ['mmmmmmmmmm']], self::overrides()->clear($attrs));
|
||
}
|
||
|
||
public function testDescribeFollowsRulingR5(): void
|
||
{
|
||
$attrs = [
|
||
'modulePreset' => ['mmmmmmmmmm'],
|
||
'groupPreset' => ['text.decoration.bodyFont' => ['presetId' => ['ffffffffff'], 'groupName' => 'divi/font-body']],
|
||
'module' => ['decoration' => ['spacing' => self::desktop(['padding' => ['top' => '40px']])]],
|
||
'text' => ['decoration' => ['bodyFont' => [
|
||
'desktop' => ['hover' => ['size' => '1px'], 'value' => ['size' => '2px']],
|
||
'phone' => ['value' => ['size' => '3px']],
|
||
]]],
|
||
];
|
||
$names = ['mmmmmmmmmm' => 'Blurb Leistung', 'ffffffffff' => 'Fließtext'];
|
||
|
||
self::assertSame(
|
||
[
|
||
's0.r0.c0.m2: lokale Werte überdecken „Blurb Leistung“: module.decoration.spacing '
|
||
. '– clear_overrides: true entfernt sie.',
|
||
's0.r0.c0.m2: lokale Werte überdecken „Fließtext“: text.decoration.bodyFont, '
|
||
. 'text.decoration.bodyFont (desktop, hover), text.decoration.bodyFont (phone) '
|
||
. '– clear_overrides: true entfernt sie.',
|
||
],
|
||
self::overrides()->describe('s0.r0.c0.m2', $attrs, $names)
|
||
);
|
||
}
|
||
|
||
public function testDescribeShowsAtMostFivePathsAndCountsTheRest(): void
|
||
{
|
||
$preset = ['id' => 'pppppppppp', 'name' => 'Viele', 'attrs' => []];
|
||
$local = ['modulePreset' => ['pppppppppp']];
|
||
foreach (range(1, 7) as $number) {
|
||
$preset['attrs']['module']['decoration']['p' . $number] = self::desktop('a');
|
||
$local['module']['decoration']['p' . $number] = self::desktop('b');
|
||
}
|
||
$overrides = new PresetOverrides(static fn (string $id): ?array => $id === 'pppppppppp' ? $preset : null);
|
||
|
||
self::assertSame(
|
||
['x1: lokale Werte überdecken „pppppppppp“: module.decoration.p1, module.decoration.p2, '
|
||
. 'module.decoration.p3, module.decoration.p4, module.decoration.p5 und 2 weitere '
|
||
. '– clear_overrides: true entfernt sie.'],
|
||
$overrides->describe('x1', $local, [])
|
||
);
|
||
}
|
||
|
||
public function testDescribeIsEmptyWithoutOverrides(): void
|
||
{
|
||
self::assertSame([], self::overrides()->describe('s0', ['modulePreset' => ['mmmmmmmmmm']], []));
|
||
}
|
||
}
|