feat: ColorArea schreibt geänderte globale Farben gemischt und stellt Snapshots ersetzend wieder her

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 14:07:12 +02:00
co-authored by Claude Sonnet 5.5
parent e269fcb1f8
commit c1f77d2017
2 changed files with 265 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
defined('ABSPATH') || exit;
/**
* The area "colors" (spec §7.1, rulings V3, V4): the global colors as ColorBook reads them through
* GlobalColorsAdapter (the five main colors from et_divi, their lastUpdated empty). A change writes only the new and
* changed colors – merged, the way Divi's builder saves –, compared with the inventory read right before; a restore
* (and a rollback) replaces all stored colors with the snapshot. Divi's sanitizing may change values: verify() only
* insists on the same colors with the same status, differences() names the colors whose values changed.
*/
final class ColorArea implements DesignArea
{
public function __construct(private GlobalColorsAdapter $colors)
{
}
public function name(): string
{
return 'colors';
}
public function label(): string
{
return 'Farben';
}
public function feature(): DiviFeature
{
return DiviFeature::ColorWrite;
}
public function raw(): array
{
return ColorBook::fromDivi($this->colors->read())->data();
}
public function rev(array $raw): string
{
return ColorBook::fromDivi($raw)->rev();
}
public function write(array $raw, bool $mayRemove): array
{
$planned = ColorBook::fromDivi($raw);
if ($mayRemove) {
$this->colors->save($planned->data(), true);
return $this->raw();
}
$changed = $planned->changedSince(ColorBook::fromDivi($this->colors->read()));
if ($changed !== []) {
$this->colors->save($changed, false);
}
return $this->raw();
}
public function restore(array $raw): array
{
return $this->write($raw, true);
}
public function verify(array $expected, array $actual): bool
{
return self::statuses($expected) === self::statuses($actual);
}
public function differences(array $expected, array $actual): array
{
$warnings = [];
foreach (ColorDiff::between(ColorBook::fromDivi($expected), ColorBook::fromDivi($actual)) as $row) {
if ($row['action'] === 'changed') {
$warnings[] = sprintf('Divi hat die Farbe „%s“ beim Speichern bereinigt.', $row['name']);
}
}
return $warnings;
}
/**
* @param array<array-key, mixed> $raw
* @return array<string, string> ID => status, sorted by ID
*/
private static function statuses(array $raw): array
{
$statuses = array_map(
static fn (array $entry): string => (string) $entry['status'],
ColorBook::fromDivi($raw)->data()
);
ksort($statuses);
return $statuses;
}
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\Divi\Design\ColorArea;
use LIT\MCP\Integrations\Divi\Design\ColorBook;
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ColorAreaTest extends TestCase
{
/**
* GlobalColorsAdapter::read(): the main color primary, the color Holz dunkel and the relative color Sand built
* on it.
*
* @return array<string, array<string, mixed>>
*/
public static function stored(): array
{
return [
'gcid-primary-color' => ['color' => '#2ea3f2', 'folder' => 'customizer', 'label' => 'Primärfarbe',
'lastUpdated' => '2026-10-19T08:00:00.000Z', 'status' => 'active', 'usedInPosts' => []],
'gcid-holz000001' => ['color' => '#5b3a29', 'folder' => '', 'label' => 'Holz dunkel',
'lastUpdated' => '2026-10-01T10:00:00.000Z', 'status' => 'active', 'usedInPosts' => []],
'gcid-sand000002' => ['color' => VariableRef::serialize('color', 'gcid-holz000001', ['lightness' => 40]),
'folder' => '', 'label' => 'Sand', 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'status' => 'active',
'usedInPosts' => []],
];
}
/**
* A GlobalColorsAdapter over $stored: save() merges (replace false) or replaces (true) like set_global_colors()
* and notes each call in $saves.
*
* @param array<string, array<string, mixed>> $stored
* @param list<array{0: array<string, mixed>, 1: bool}> $saves
*/
public static function adapter(array &$stored, array &$saves): GlobalColorsAdapter
{
$colors = Mockery::mock(GlobalColorsAdapter::class);
$colors->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
$colors->allows('save')->andReturnUsing(
static function (array $entries, bool $replace) use (&$stored, &$saves): void {
$saves[] = [$entries, $replace];
$stored = $replace ? $entries : array_merge($stored, $entries);
}
);
return $colors;
}
/**
* @param array<string, array<string, mixed>> $stored
* @param array<string, mixed> $fields
* @return array<string, array<string, mixed>>
*/
private static function changed(array $stored, string $id, array $fields): array
{
$stored[$id] = $fields + $stored[$id];
return $stored;
}
/** @return array<string, mixed> */
private static function white(): array
{
return ['color' => '#ffffff', 'folder' => '', 'label' => 'Weiß', 'lastUpdated' => '2026-10-19T09:00:00.000Z',
'status' => 'active', 'usedInPosts' => []];
}
public function testAreaReadsThroughTheAdapter(): void
{
$stored = self::stored();
$saves = [];
$area = new ColorArea(self::adapter($stored, $saves));
self::assertSame(['colors', 'Farben'], [$area->name(), $area->label()]);
self::assertSame(DiviFeature::ColorWrite, $area->feature());
self::assertSame(ColorBook::fromDivi($stored)->data(), $area->raw());
self::assertSame(ColorBook::fromDivi($stored)->rev(), $area->rev($stored));
}
/** Ruling V4: only new and changed colors are merged; the unchanged main color is not written again. */
public function testWriteSavesOnlyNewAndChangedColorsAndReadsBack(): void
{
$stored = self::stored();
$saves = [];
$area = new ColorArea(self::adapter($stored, $saves));
$planned = self::changed(self::stored(), 'gcid-holz000001', ['color' => '#000000']);
$planned['gcid-neu0000003'] = self::white();
$saved = $area->write($planned, false);
self::assertCount(1, $saves);
self::assertFalse($saves[0][1], 'merge, never replace');
$keys = array_keys($saves[0][0]);
sort($keys);
self::assertSame(['gcid-holz000001', 'gcid-neu0000003'], $keys);
self::assertSame('#000000', $saved['gcid-holz000001']['color']);
self::assertSame(ColorBook::fromDivi($stored)->data(), $saved);
}
public function testWriteWithoutChangesSavesNothing(): void
{
$stored = self::stored();
$saves = [];
(new ColorArea(self::adapter($stored, $saves)))->write(self::stored(), false);
self::assertSame([], $saves);
}
/** Ruling V4: a restore (and a rollback) replaces all stored colors with the snapshot. */
public function testRestoreReplacesAllColorsWithTheSnapshot(): void
{
$stored = self::changed(self::stored(), 'gcid-holz000001', ['color' => '#000000']);
$stored['gcid-neu0000003'] = self::white();
$saves = [];
$area = new ColorArea(self::adapter($stored, $saves));
$snapshot = ColorBook::fromDivi(self::stored())->data();
$saved = $area->restore($snapshot);
self::assertSame([[$snapshot, true]], $saves);
self::assertSame($snapshot, $saved);
}
public function testVerifyInsistsOnTheSameColorsWithTheSameStatus(): void
{
$stored = self::stored();
$saves = [];
$area = new ColorArea(self::adapter($stored, $saves));
$expected = self::stored();
$missing = $expected;
unset($missing['gcid-sand000002']);
self::assertTrue($area->verify($expected, self::changed($expected, 'gcid-holz000001', ['color' => '#5B3A29'])));
self::assertFalse($area->verify($expected, $missing));
$inactive = self::changed($expected, 'gcid-holz000001', ['status' => 'inactive']);
self::assertFalse($area->verify($expected, $inactive));
}
public function testDifferencesNameTheColorsDiviChanged(): void
{
$stored = self::stored();
$saves = [];
$area = new ColorArea(self::adapter($stored, $saves));
$expected = self::stored();
self::assertSame([], $area->differences($expected, $expected));
self::assertSame(
['Divi hat die Farbe „Holz dunkel“ beim Speichern bereinigt.'],
$area->differences($expected, self::changed($expected, 'gcid-holz000001', ['color' => '#5B3A29']))
);
}
}