diff --git a/src/Integrations/Divi/Design/VariableArea.php b/src/Integrations/Divi/Design/VariableArea.php new file mode 100644 index 0000000..14307d1 --- /dev/null +++ b/src/Integrations/Divi/Design/VariableArea.php @@ -0,0 +1,106 @@ +variables->read())->data(); + } + + public function rev(array $raw): string + { + return VariableBook::fromDivi($raw)->rev(); + } + + /** $mayRemove has no effect: variables are never removed (ruling V1). */ + public function write(array $raw, bool $mayRemove): array + { + $this->variables->save(VariableBook::fromDivi($raw)->data(), $this->variables->read()); + + return $this->raw(); + } + + public function restore(array $raw): array + { + $live = $this->variables->read(); + $this->variables->save( + VariableBook::fromDivi($live)->restoredFrom(VariableBook::fromDivi($raw))->data(), + $live + ); + + return $this->raw(); + } + + public function verify(array $expected, array $actual): bool + { + return self::shape($expected) === self::shape($actual); + } + + public function differences(array $expected, array $actual): array + { + $warnings = []; + foreach (VariableDiff::between(VariableBook::fromDivi($expected), VariableBook::fromDivi($actual)) as $row) { + if ($row['action'] === 'changed') { + $warnings[] = sprintf('Divi hat den Wert der Variablen „%s“ beim Speichern bereinigt.', $row['name']); + } + } + + return $warnings; + } + + /** + * @param array $raw + * @return array "type/id" => [status, has a value], sorted + */ + private static function shape(array $raw): array + { + $shape = []; + foreach (VariableBook::fromDivi($raw)->data() as $type => $entries) { + foreach ($entries as $id => $entry) { + $value = $entry['value'] ?? ''; + $shape[$type . '/' . $id] = [ + (string) ($entry['status'] ?? ''), + is_scalar($value) && (string) $value !== '', + ]; + } + } + ksort($shape); + + return $shape; + } +} diff --git a/tests/Unit/Integrations/Divi/Design/VariableAreaTest.php b/tests/Unit/Integrations/Divi/Design/VariableAreaTest.php new file mode 100644 index 0000000..c57dba8 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/VariableAreaTest.php @@ -0,0 +1,159 @@ +>> + */ + public static function stored(): array + { + $entry = static fn (string $type, string $id, string $label, string $value, string $order, string $status) + : array => ['id' => $id, 'label' => $label, 'value' => $value, 'order' => $order, 'status' => $status, + 'lastUpdated' => '2026-10-01T10:00:00.000Z', 'variableType' => $type]; + + return [ + 'numbers' => [ + 'gvid-radius0001' => $entry('numbers', 'gvid-radius0001', 'Eckenradius', '8px', '1', 'active'), + ], + 'strings' => [ + 'gvid-slogan0001' => $entry('strings', 'gvid-slogan0001', 'Slogan', 'Holz mit Herz', '1', 'active'), + 'gvid-alt0000001' => $entry('strings', 'gvid-alt0000001', 'Alter Slogan', 'Früher', '', 'archived'), + 'gvid-leer000001' => $entry('strings', 'gvid-leer000001', 'Leer', '', '', 'archived'), + ], + 'gradients' => [ + 'gvid-verlauf001' => $entry( + 'gradients', + 'gvid-verlauf001', + 'Verlauf', + 'linear-gradient(#fff, #000)', + '1', + 'active' + ), + ], + ]; + } + + /** + * A GlobalVariablesAdapter over $stored: save() replaces the inventory like set_global_variables() and notes each + * call (data, expected) in $saves. + * + * @param array>> $stored + * @param list, 1: array}> $saves + */ + public static function adapter(array &$stored, array &$saves, bool $manage = true): GlobalVariablesAdapter + { + $variables = Mockery::mock(GlobalVariablesAdapter::class); + $variables->allows('canManage')->andReturn($manage); + $variables->allows('read')->andReturnUsing(static function () use (&$stored): array { + return $stored; + }); + $variables->allows('save')->andReturnUsing( + static function (array $data, array $expected) use (&$stored, &$saves): void { + $saves[] = [$data, $expected]; + $stored = $data; + } + ); + + return $variables; + } + + public function testAreaReadsThroughTheAdapter(): void + { + $stored = self::stored(); + $saves = []; + $area = new VariableArea(self::adapter($stored, $saves)); + + self::assertSame(['variables', 'Variablen'], [$area->name(), $area->label()]); + self::assertSame(DiviFeature::VariableWrite, $area->feature()); + self::assertSame(VariableBook::fromDivi($stored)->data(), $area->raw()); + self::assertSame(VariableBook::fromDivi($stored)->rev(), $area->rev($stored)); + } + + /** Ruling V7: the whole planned inventory, with the inventory read before for Divi's stale check. */ + public function testWriteSavesThePlannedInventoryAgainstTheLiveOne(): void + { + $stored = self::stored(); + $saves = []; + $area = new VariableArea(self::adapter($stored, $saves)); + $planned = self::stored(); + $planned['numbers']['gvid-radius0001']['value'] = '12px'; + + $saved = $area->write($planned, false); + + self::assertSame([[VariableBook::fromDivi($planned)->data(), self::stored()]], $saves); + self::assertSame('12px', $saved['numbers']['gvid-radius0001']['value']); + } + + /** Ruling V1b: Divi refuses an inventory without a stored ID – later variables stay, archived. */ + public function testRestoreKeepsVariablesCreatedAfterTheSnapshotArchived(): void + { + $stored = self::stored(); + $stored['numbers']['gvid-neu0000001'] = ['id' => 'gvid-neu0000001', 'label' => 'Neu', 'value' => '4px', + 'order' => '2', 'status' => 'active', 'lastUpdated' => '2026-10-19T09:00:00.000Z', + 'variableType' => 'numbers']; + $live = $stored; + $saves = []; + $area = new VariableArea(self::adapter($stored, $saves)); + + $area->restore(VariableBook::fromDivi(self::stored())->data()); + + self::assertCount(1, $saves); + self::assertSame( + VariableBook::fromDivi($live)->restoredFrom(VariableBook::fromDivi(self::stored()))->data(), + $saves[0][0] + ); + self::assertSame($live, $saves[0][1]); + self::assertSame('archived', $saves[0][0]['numbers']['gvid-neu0000001']['status']); + } + + public function testVerifyInsistsOnTheSameVariablesStatusAndValues(): void + { + $stored = self::stored(); + $saves = []; + $area = new VariableArea(self::adapter($stored, $saves)); + $expected = self::stored(); + $sanitized = $expected; + $sanitized['strings']['gvid-slogan0001']['value'] = 'Holz mit Herz '; + $missing = $expected; + unset($missing['numbers']['gvid-radius0001']); + $archived = $expected; + $archived['numbers']['gvid-radius0001']['status'] = 'archived'; + $emptied = $expected; + $emptied['numbers']['gvid-radius0001']['value'] = ''; + + self::assertTrue($area->verify($expected, $sanitized)); + self::assertFalse($area->verify($expected, $missing)); + self::assertFalse($area->verify($expected, $archived)); + self::assertFalse($area->verify($expected, $emptied)); + } + + public function testDifferencesNameTheVariablesDiviChanged(): void + { + $stored = self::stored(); + $saves = []; + $area = new VariableArea(self::adapter($stored, $saves)); + $expected = self::stored(); + $sanitized = $expected; + $sanitized['strings']['gvid-slogan0001']['value'] = 'Holz mit Herz '; + + self::assertSame([], $area->differences($expected, $expected)); + self::assertSame( + ['Divi hat den Wert der Variablen „Slogan“ beim Speichern bereinigt.'], + $area->differences($expected, $sanitized) + ); + } +}