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:
co-authored by
Claude Sonnet 5.5
parent
e269fcb1f8
commit
c1f77d2017
@@ -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']))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user