Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
200 lines
7.9 KiB
PHP
200 lines
7.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||
|
||
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\Compose\Warnings;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
final class WarningsTest extends TestCase
|
||
{
|
||
public function testWarnsAboutTheTouchedNodesOnly(): void
|
||
{
|
||
$composed = ComposeFixture::composer()->compose([
|
||
['type' => 'section', 'label' => 'Test', 'modules' => [
|
||
['type' => 'heading', 'text' => 'Titel', 'level' => 'h1'],
|
||
['type' => 'heading', 'text' => 'Zu tief', 'level' => 'h4'],
|
||
['type' => 'image', 'src' => 'https://kunde.test/a.jpg'],
|
||
['type' => 'text', 'content' => ''],
|
||
['type' => 'code', 'content' => '<div>x</div>'],
|
||
['type' => 'accordion'],
|
||
['type' => 'button', 'text' => 'Los', 'attrs' => [
|
||
'button.decoration.background' => ['desktop' => ['color' => '#2ea3f3']],
|
||
'button.decoration.font.font' => ['desktop' => ['family' => 'Open Sans', 'color' => '#abcdef']],
|
||
]],
|
||
]],
|
||
], null, 'nodes');
|
||
self::assertSame([], $composed['errors']);
|
||
$document = new Document();
|
||
foreach ($composed['blocks'] as $block) {
|
||
$document->append($block);
|
||
}
|
||
|
||
$warnings = (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), $composed['blocks']);
|
||
|
||
self::assertSame(
|
||
[
|
||
's0.r0.c0.m2: Bild ohne Alt-Text.',
|
||
's0.r0.c0.m3: text ist leer (content fehlt).',
|
||
's0.r0.c0.m4: Code-Modul verwendet – im Divi-Builder nicht visuell bearbeitbar; nur nutzen, wenn kein '
|
||
. 'natives Modul passt.',
|
||
's0.r0.c0.m5: accordion ohne Einträge.',
|
||
's0.r0.c0.m6: feste Farbe #2ea3f3 – globale Farbe "Primärfarbe" ({{color:primary}}) passt.',
|
||
's0.r0.c0.m6: feste Schrift Open Sans – {{font:heading}} verwenden.',
|
||
's0.r0.c0.m1: Sprung in der Überschriftenhierarchie (h4 nach h1).',
|
||
],
|
||
$warnings
|
||
);
|
||
|
||
self::assertSame([], (new Warnings(DiviFixtures::designSystem()))->collect(NodeIndex::build($document), []));
|
||
}
|
||
|
||
/** @return array<string, mixed> Button attrs with a preset and a local font color. */
|
||
private static function button(string $color, string $preset = 'p-2'): array
|
||
{
|
||
return [
|
||
'modulePreset' => [$preset],
|
||
'button' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['color' => $color]]]]]],
|
||
];
|
||
}
|
||
|
||
/** Preset p-2 "CTA primär" sets the font color #ffffff; g-1 is a group preset without values. */
|
||
private static function book(): PresetBook
|
||
{
|
||
return PresetBook::fromDivi([
|
||
'module' => ['divi/button' => ['default' => '', 'items' => [
|
||
'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'],
|
||
]]],
|
||
]);
|
||
}
|
||
|
||
/** @param int $loads Counts how often the preset book is read. */
|
||
private static function presets(int &$loads): PresetAssignment
|
||
{
|
||
return ComposeFixture::presetAssignment(null, null, null, null, static function () use (&$loads): PresetBook {
|
||
++$loads;
|
||
|
||
return self::book();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param list<Block> $blocks top-level blocks of the page
|
||
* @param list<Block> $touched
|
||
* @return array{0: list<string>, 1: NodeIndex}
|
||
*/
|
||
private static function collect(array $blocks, array $touched, PresetAssignment $presets): array
|
||
{
|
||
$document = new Document();
|
||
foreach ($blocks as $block) {
|
||
$document->append($block);
|
||
}
|
||
$index = NodeIndex::build($document);
|
||
$warnings = (new Warnings(DiviFixtures::designSystem(), $presets))->collect($index, $touched);
|
||
|
||
return [
|
||
array_values(array_filter($warnings, static fn (string $w): bool => str_contains($w, 'überdecken'))),
|
||
$index,
|
||
];
|
||
}
|
||
|
||
public function testWarnsWhenLocalValuesCoverAnAssignedPreset(): void
|
||
{
|
||
$loads = 0;
|
||
$button = Block::create('divi/button', self::button('#000000'));
|
||
|
||
[$covering, $index] = self::collect([$button], [$button], self::presets($loads));
|
||
|
||
self::assertCount(1, $covering);
|
||
self::assertStringStartsWith(
|
||
$index->idOf($button) . ': lokale Werte überdecken „CTA primär“: ',
|
||
$covering[0]
|
||
);
|
||
self::assertStringContainsString('button.decoration.font.font', $covering[0]);
|
||
self::assertStringEndsWith('clear_overrides: true entfernt sie.', $covering[0]);
|
||
}
|
||
|
||
public function testEqualValuesAndUntouchedBlocksDoNotWarn(): void
|
||
{
|
||
$loads = 0;
|
||
$same = Block::create('divi/button', self::button('#ffffff'));
|
||
$other = Block::create('divi/button', self::button('#000000'));
|
||
|
||
[$covering] = self::collect([$same, $other], [$same], self::presets($loads));
|
||
|
||
self::assertSame([], $covering, 'the same value covers nothing; $other was not touched');
|
||
}
|
||
|
||
public function testChildrenOfATouchedBlockAreChecked(): void
|
||
{
|
||
$loads = 0;
|
||
$button = Block::create('divi/button', self::button('#000000'));
|
||
$section = Block::create('divi/section', [], [
|
||
Block::create('divi/row', [], [Block::create('divi/column', [], [$button])]),
|
||
]);
|
||
|
||
[$covering, $index] = self::collect([$section], [$section], self::presets($loads));
|
||
|
||
self::assertCount(1, $covering);
|
||
self::assertStringStartsWith($index->idOf($button) . ': lokale Werte', $covering[0]);
|
||
}
|
||
|
||
public function testThePresetBookIsOnlyReadForBlocksThatHavePresets(): void
|
||
{
|
||
$loads = 0;
|
||
$plain = Block::create('divi/button', [
|
||
'button' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['color' => '#000000']]]]]],
|
||
]);
|
||
$presets = self::presets($loads);
|
||
|
||
self::collect([$plain], [$plain], $presets);
|
||
self::assertSame(0, $loads, 'no touched block names a preset');
|
||
|
||
$grouped = Block::create('divi/button', [
|
||
'groupPreset' => ['button.decoration.font.font' => ['groupName' => 'divi/font', 'presetId' => ['g-1']]],
|
||
]);
|
||
$stacked = Block::create('divi/button', self::button('#000000'));
|
||
[$covering] = self::collect([$plain, $grouped, $stacked], [$plain, $grouped, $stacked], $presets);
|
||
|
||
self::assertSame(1, $loads, 'read once, for the first block that has a preset');
|
||
self::assertCount(1, $covering);
|
||
}
|
||
|
||
public function testOpaqueAndForeignBlocksAreSkipped(): void
|
||
{
|
||
$loads = 0;
|
||
$attrs = self::button('#000000');
|
||
$opaque = Block::parsed(
|
||
'divi/button',
|
||
$attrs + ['globalModule' => '9'],
|
||
'<!-- wp:divi/button {"globalModule":"9"} /-->',
|
||
true,
|
||
true
|
||
);
|
||
$foreign = Block::create('core/paragraph', $attrs);
|
||
|
||
[$covering] = self::collect([$opaque, $foreign], [$opaque, $foreign], self::presets($loads));
|
||
|
||
self::assertSame([], $covering);
|
||
self::assertSame(0, $loads);
|
||
}
|
||
}
|