diff --git a/src/Integrations/Divi/Design/ColorArea.php b/src/Integrations/Divi/Design/ColorArea.php new file mode 100644 index 0000000..d5e9e04 --- /dev/null +++ b/src/Integrations/Divi/Design/ColorArea.php @@ -0,0 +1,102 @@ +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 $raw + * @return array 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; + } +} diff --git a/tests/Unit/Integrations/Divi/Design/ColorAreaTest.php b/tests/Unit/Integrations/Divi/Design/ColorAreaTest.php new file mode 100644 index 0000000..7c2c1b1 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/ColorAreaTest.php @@ -0,0 +1,163 @@ +> + */ + 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> $stored + * @param list, 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> $stored + * @param array $fields + * @return array> + */ + private static function changed(array $stored, string $id, array $fields): array + { + $stored[$id] = $fields + $stored[$id]; + + return $stored; + } + + /** @return array */ + 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'])) + ); + } +}