feat: ColorRestorePlanner und VariableRestorePlanner – Restore-Pläne für Farben und Variablen (V1b, V10)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
5af188d656
commit
bd2043564b
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Restores a snapshot of the global colors (ruling V10): the snapshot replaces every stored color
|
||||
* (set_global_colors($data, false), ruling V4) – colors created after the snapshot are removed. ColorDiff knows no
|
||||
* removal, so the planner adds those rows ("removed") itself and warns that pages keep their references.
|
||||
*/
|
||||
final class ColorRestorePlanner implements RestorePlanner
|
||||
{
|
||||
private const REMOVED = 'Farben, die es im Snapshot nicht gibt, entfallen (%s); Seiten und Presets, die sie '
|
||||
. 'verwenden, behalten den Verweis ohne Wert.';
|
||||
|
||||
public function __construct(private DesignSystemWriter $writer, private GlobalColorsAdapter $colors)
|
||||
{
|
||||
}
|
||||
|
||||
public function area(): string
|
||||
{
|
||||
return 'colors';
|
||||
}
|
||||
|
||||
public function designArea(): DesignArea
|
||||
{
|
||||
return new ColorArea($this->colors);
|
||||
}
|
||||
|
||||
public function rights(): ?WP_Error
|
||||
{
|
||||
return DesignRights::colors($this->writer);
|
||||
}
|
||||
|
||||
/** Writes the colors (ColorWrite), reading the design system first (DesignRead). */
|
||||
public function features(): array
|
||||
{
|
||||
return [DiviFeature::DesignRead, DiviFeature::ColorWrite];
|
||||
}
|
||||
|
||||
public function plan(array $current, array $snapshot, string $snapshotId): DesignPlan|WP_Error
|
||||
{
|
||||
$before = ColorBook::fromDivi($current);
|
||||
$after = ColorBook::fromDivi($snapshot);
|
||||
$kept = array_values(array_filter(
|
||||
ColorDiff::between($before, $after),
|
||||
static fn (array $row): bool => $after->entry($row['id']) !== null
|
||||
));
|
||||
$removed = [];
|
||||
foreach (array_keys(array_diff_key($before->data(), $after->data())) as $id) {
|
||||
$removed[] = ['id' => (string) $id, 'name' => $before->label((string) $id), 'action' => 'removed'];
|
||||
}
|
||||
|
||||
$summary = sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, ColorDiff::summary($kept));
|
||||
$warnings = [];
|
||||
if ($removed !== []) {
|
||||
$summary .= sprintf(' Entfernt: %d.', count($removed));
|
||||
$warnings[] = sprintf(
|
||||
self::REMOVED,
|
||||
implode(', ', array_map(static fn (array $row): string => '„' . $row['name'] . '“', $removed))
|
||||
);
|
||||
}
|
||||
|
||||
return new DesignPlan($after->data(), array_merge($kept, $removed), $summary, null, $warnings, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use WP_Error;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Restores a snapshot of the global variables (rulings V1b, V10): every variable of the snapshot comes back as it was;
|
||||
* Divi refuses to drop a stored variable, so variables created after the snapshot stay – archived
|
||||
* (VariableBook::restoredFrom()) – and the dry run names them as a warning. Nothing is removed.
|
||||
*/
|
||||
final class VariableRestorePlanner implements RestorePlanner
|
||||
{
|
||||
private const KEPT = 'Variable „%s“ gibt es im Snapshot nicht; sie bleibt archiviert erhalten (Divi löscht keine '
|
||||
. 'Variablen).';
|
||||
|
||||
public function __construct(private DesignSystemWriter $writer, private GlobalVariablesAdapter $variables)
|
||||
{
|
||||
}
|
||||
|
||||
public function area(): string
|
||||
{
|
||||
return 'variables';
|
||||
}
|
||||
|
||||
public function designArea(): DesignArea
|
||||
{
|
||||
return new VariableArea($this->variables);
|
||||
}
|
||||
|
||||
public function rights(): ?WP_Error
|
||||
{
|
||||
return DesignRights::variables($this->writer, $this->variables);
|
||||
}
|
||||
|
||||
/** Writes the variables (VariableWrite), reading the design system first (DesignRead). */
|
||||
public function features(): array
|
||||
{
|
||||
return [DiviFeature::DesignRead, DiviFeature::VariableWrite];
|
||||
}
|
||||
|
||||
public function plan(array $current, array $snapshot, string $snapshotId): DesignPlan|WP_Error
|
||||
{
|
||||
$before = VariableBook::fromDivi($current);
|
||||
$older = VariableBook::fromDivi($snapshot);
|
||||
$after = $before->restoredFrom($older);
|
||||
$diff = VariableDiff::between($before, $after);
|
||||
|
||||
return new DesignPlan(
|
||||
$after->data(),
|
||||
$diff,
|
||||
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, VariableDiff::summary($diff)),
|
||||
null,
|
||||
array_map(static fn (array $row): string => sprintf(self::KEPT, $row['label']), $before->onlyHere($older))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Design\ColorArea;
|
||||
use LIT\MCP\Integrations\Divi\Design\ColorBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\ColorRestorePlanner;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignPlan;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class ColorRestorePlannerTest extends TestCase
|
||||
{
|
||||
public const SNAPSHOT_ID = 'colors-20261016T100000Z-aaaaaa';
|
||||
|
||||
/** One stored color in ColorBook::data() form. */
|
||||
public static function color(string $label, string $color): array
|
||||
{
|
||||
return ['color' => $color, 'folder' => '', 'label' => $label, 'lastUpdated' => '2026-10-01T10:00:00.000Z',
|
||||
'status' => 'active', 'usedInPosts' => []];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> The colors now: "Marke" red, "Akzent" created after the snapshot. */
|
||||
public static function current(): array
|
||||
{
|
||||
return [
|
||||
'gcid-brand00001' => self::color('Marke', '#ff0000'),
|
||||
'gcid-akzent0001' => self::color('Akzent', '#00ff00'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> The snapshot: "Marke" blue, no "Akzent". */
|
||||
public static function snapshot(): array
|
||||
{
|
||||
return ['gcid-brand00001' => self::color('Marke', '#0000ff')];
|
||||
}
|
||||
|
||||
private static function planner(bool $visualBuilder = true): ColorRestorePlanner
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn($visualBuilder);
|
||||
|
||||
return new ColorRestorePlanner($writer, Mockery::mock(GlobalColorsAdapter::class));
|
||||
}
|
||||
|
||||
public function testAreaFeaturesAndDesignArea(): void
|
||||
{
|
||||
$planner = self::planner();
|
||||
|
||||
self::assertSame('colors', $planner->area());
|
||||
self::assertSame([DiviFeature::DesignRead, DiviFeature::ColorWrite], $planner->features());
|
||||
self::assertInstanceOf(ColorArea::class, $planner->designArea());
|
||||
}
|
||||
|
||||
public function testNeedsTheColorRights(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
self::assertNull(self::planner()->rights());
|
||||
self::assertToolError(self::planner(false)->rights(), 'forbidden');
|
||||
}
|
||||
|
||||
public function testTheSnapshotReplacesEveryColorAndNamesTheRemovedOnes(): void
|
||||
{
|
||||
$plan = self::planner()->plan(self::current(), self::snapshot(), self::SNAPSHOT_ID);
|
||||
|
||||
self::assertInstanceOf(DesignPlan::class, $plan);
|
||||
self::assertTrue($plan->mayRemove);
|
||||
self::assertSame(ColorBook::fromDivi(self::snapshot())->data(), $plan->raw);
|
||||
self::assertSame(
|
||||
[['gcid-brand00001', 'changed'], ['gcid-akzent0001', 'removed']],
|
||||
array_map(static fn (array $row): array => [$row['id'], $row['action']], $plan->diff)
|
||||
);
|
||||
self::assertStringStartsWith('Snapshot ' . self::SNAPSHOT_ID . ' wiederherstellen – Farben: ', $plan->summary);
|
||||
self::assertStringEndsWith(' Entfernt: 1.', $plan->summary);
|
||||
self::assertSame(
|
||||
['Farben, die es im Snapshot nicht gibt, entfallen („Akzent“); Seiten und Presets, die sie verwenden, '
|
||||
. 'behalten den Verweis ohne Wert.'],
|
||||
$plan->warnings
|
||||
);
|
||||
}
|
||||
|
||||
public function testTheSameColorsAreNoChange(): void
|
||||
{
|
||||
$plan = self::planner()->plan(self::snapshot(), self::snapshot(), self::SNAPSHOT_ID);
|
||||
|
||||
self::assertInstanceOf(DesignPlan::class, $plan);
|
||||
self::assertTrue($plan->isEmpty());
|
||||
self::assertSame([], $plan->warnings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignPlan;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableArea;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\VariableRestorePlanner;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class VariableRestorePlannerTest extends TestCase
|
||||
{
|
||||
public const SNAPSHOT_ID = 'variables-20261016T100000Z-aaaaaa';
|
||||
|
||||
/** One stored variable of type numbers. */
|
||||
public static function number(string $id, string $label, string $value): array
|
||||
{
|
||||
return ['id' => $id, 'label' => $label, 'value' => $value, 'order' => '1', 'status' => 'active',
|
||||
'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => 'numbers'];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, array<string, mixed>>> Now: radius 8px and "Neu", created afterwards. */
|
||||
public static function current(): array
|
||||
{
|
||||
return ['numbers' => [
|
||||
'gvid-radius0001' => self::number('gvid-radius0001', 'Eckenradius', '8px'),
|
||||
'gvid-neu0000001' => self::number('gvid-neu0000001', 'Neu', '2rem'),
|
||||
]];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, array<string, mixed>>> The snapshot: radius 4px. */
|
||||
public static function snapshot(): array
|
||||
{
|
||||
return ['numbers' => ['gvid-radius0001' => self::number('gvid-radius0001', 'Eckenradius', '4px')]];
|
||||
}
|
||||
|
||||
private static function planner(bool $manage = true): VariableRestorePlanner
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$variables = Mockery::mock(GlobalVariablesAdapter::class);
|
||||
$variables->allows('canManage')->andReturn($manage);
|
||||
|
||||
return new VariableRestorePlanner($writer, $variables);
|
||||
}
|
||||
|
||||
public function testAreaFeaturesAndDesignArea(): void
|
||||
{
|
||||
$planner = self::planner();
|
||||
|
||||
self::assertSame('variables', $planner->area());
|
||||
self::assertSame([DiviFeature::DesignRead, DiviFeature::VariableWrite], $planner->features());
|
||||
self::assertInstanceOf(VariableArea::class, $planner->designArea());
|
||||
}
|
||||
|
||||
public function testNeedsTheVariableRights(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
self::assertNull(self::planner()->rights());
|
||||
self::assertToolError(self::planner(false)->rights(), 'forbidden');
|
||||
}
|
||||
|
||||
public function testRestoresTheSnapshotAndKeepsNewerVariablesArchived(): void
|
||||
{
|
||||
$current = VariableBook::fromDivi(self::current());
|
||||
$snapshot = VariableBook::fromDivi(self::snapshot());
|
||||
|
||||
$plan = self::planner()->plan(self::current(), self::snapshot(), self::SNAPSHOT_ID);
|
||||
|
||||
self::assertInstanceOf(DesignPlan::class, $plan);
|
||||
self::assertFalse($plan->mayRemove, 'V1: variables are never removed');
|
||||
self::assertSame($current->restoredFrom($snapshot)->data(), $plan->raw);
|
||||
self::assertSame('4px', $plan->raw['numbers']['gvid-radius0001']['value']);
|
||||
self::assertSame('archived', $plan->raw['numbers']['gvid-neu0000001']['status']);
|
||||
self::assertEqualsCanonicalizing(
|
||||
[['gvid-radius0001', 'changed'], ['gvid-neu0000001', 'archived']],
|
||||
array_map(static fn (array $row): array => [$row['id'], $row['action']], $plan->diff)
|
||||
);
|
||||
self::assertStringStartsWith('Snapshot ' . self::SNAPSHOT_ID . ' wiederherstellen – Variablen: ', $plan->summary);
|
||||
self::assertSame(
|
||||
['Variable „Neu“ gibt es im Snapshot nicht; sie bleibt archiviert erhalten (Divi löscht keine Variablen).'],
|
||||
$plan->warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user