feat: Warnung, wenn lokale Werte ein zugewiesenes Preset überdecken
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
be6ca5bee0
commit
1f9b50f1a6
@@ -9,6 +9,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
@@ -16,8 +17,9 @@ defined('ABSPATH') || exit;
|
||||
/**
|
||||
* Quality warnings of a Divi write (spec §5.5) – only about the nodes this call created or
|
||||
* changed (and their children): fixed colors/fonts where a global one fits, images without
|
||||
* alt text, empty modules, heading jumps, the code module. Similar existing sections
|
||||
* (lit/library-find-similar) follow with the Library in Etappe 3. Pure PHP.
|
||||
* alt text, empty modules, heading jumps, the code module, local values that cover an assigned
|
||||
* preset (ruling R5). Similar existing sections (lit/library-find-similar) follow with the Library
|
||||
* in Etappe 3. Pure PHP.
|
||||
*/
|
||||
final class Warnings
|
||||
{
|
||||
@@ -33,7 +35,7 @@ final class Warnings
|
||||
'divi/slide' => ['image.innerContent', 'src', 'alt'],
|
||||
];
|
||||
|
||||
public function __construct(private DesignSystem $design)
|
||||
public function __construct(private DesignSystem $design, private ?PresetAssignment $presets = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -58,7 +60,8 @@ final class Warnings
|
||||
...$this->code($block, $id),
|
||||
...$this->image($block, $id),
|
||||
...$this->empty($block, $id),
|
||||
...$this->fixedValues($block, $id)
|
||||
...$this->fixedValues($block, $id),
|
||||
...$this->coveredPresets($block, $id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,6 +164,27 @@ final class Warnings
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling R5: local values that cover an explicitly assigned preset of the block. Only Divi blocks that name a
|
||||
* preset cost work (opaque blocks – global instances – are never looked into); the preset book is read when the
|
||||
* first such block comes up, never for a page without presets.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function coveredPresets(Block $block, string $id): array
|
||||
{
|
||||
if ($this->presets === null || ! $block->isDivi() || $block->isOpaque() || ! self::namesPresets($block)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->presets->overrides()->describe($id, $block->attrs(), $this->presets->names());
|
||||
}
|
||||
|
||||
private static function namesPresets(Block $block): bool
|
||||
{
|
||||
return NodeSummary::presetIds($block) !== [] || NodeSummary::groupPresets($block) !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Heading jumps (h2 → h4) in page order where the later heading belongs to a touched block.
|
||||
*
|
||||
|
||||
@@ -160,6 +160,6 @@ class DiviComposition
|
||||
|
||||
public function warnings(): Warnings
|
||||
{
|
||||
return new Warnings($this->design->read());
|
||||
return new Warnings($this->design->read(), $this->presets());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,21 +247,24 @@ final class ComposeFixture
|
||||
|
||||
/**
|
||||
* PresetAssignment as the live wiring builds it: the book is $book (default: bookOf($design), else of the sample
|
||||
* design system), the id check is the cleaner's keepsId().
|
||||
* design system), the id check is the cleaner's keepsId(). $load replaces $book with a loader the test can
|
||||
* count.
|
||||
*
|
||||
* @param (Closure(string): array<string, string>)|null $groupsOf
|
||||
* @param (Closure(): PresetBook)|null $load
|
||||
*/
|
||||
public static function presetAssignment(
|
||||
?PresetBook $book = null,
|
||||
?Closure $groupsOf = null,
|
||||
?DesignSystem $design = null,
|
||||
?ValueCleaner $cleaner = null
|
||||
?ValueCleaner $cleaner = null,
|
||||
?Closure $load = null
|
||||
): PresetAssignment {
|
||||
$book ??= self::bookOf($design ?? DiviFixtures::designSystem());
|
||||
$cleaner ??= self::cleaner();
|
||||
|
||||
return new PresetAssignment(
|
||||
static fn (): PresetBook => $book,
|
||||
$load ?? static fn (): PresetBook => $book,
|
||||
$groupsOf ?? static fn (string $module): array => [],
|
||||
$cleaner->keepsId(...)
|
||||
);
|
||||
|
||||
@@ -4,9 +4,12 @@ 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;
|
||||
@@ -53,4 +56,144 @@ final class WarningsTest extends TestCase
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,14 @@ final class DiviCompositionTest extends TestCase
|
||||
$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'],
|
||||
'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'],
|
||||
@@ -238,4 +245,24 @@ final class DiviCompositionTest extends TestCase
|
||||
self::assertNotSame($seven->presets(), $base->forPost(8)->presets());
|
||||
self::assertNotSame($base->presets(), $seven->presets());
|
||||
}
|
||||
|
||||
public function testWarningsKnowTheWritersPresets(): void
|
||||
{
|
||||
$composition = $this->composition($this->presetSource())->forPost(7);
|
||||
$button = Block::create('divi/button', [
|
||||
'modulePreset' => ['p-2'],
|
||||
'button' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => ['color' => '#000000']]]]]],
|
||||
]);
|
||||
$document = new Document();
|
||||
$document->append($button);
|
||||
$index = NodeIndex::build($document);
|
||||
|
||||
$warnings = $composition->warnings()->collect($index, [$button]);
|
||||
|
||||
$covering = array_values(
|
||||
array_filter($warnings, static fn (string $w): bool => str_contains($w, 'überdecken'))
|
||||
);
|
||||
self::assertCount(1, $covering);
|
||||
self::assertStringContainsString('„CTA primär“', $covering[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user