feat: ThemeOptionsArea – Bereich theme_options ersetzt die Whitelist, prüft das Zurücklesen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 17:51:09 +02:00
co-authored by Claude Sonnet 5.5
parent 05ff2d90f3
commit 1bc8ce65b7
2 changed files with 263 additions and 0 deletions
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Closure;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionBook;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsArea;
use LIT\MCP\Integrations\Divi\Design\ThemeOptionSpec;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ThemeOptionsAreaTest extends TestCase
{
/**
* ThemeOptionsAdapter::read(): body font, text size and line height set; everything else at Divi's default.
*
* @return array<string, int|float|string|false>
*/
public static function stored(): array
{
return ['body_font' => 'Open Sans', 'body_font_size' => 16, 'body_font_height' => 1.8];
}
/**
* A ThemeOptionsAdapter over $stored: write() sets and deletes like et_update_option/et_delete_option, notes each
* call in $writes and returns the read-back; sanitize() runs $sanitize (default: Divi keeps the value).
*
* @param array<string, int|float|string|false> $stored
* @param list<array{0: array<string, int|float|string|false>, 1: list<string>}> $writes
* @param (Closure(string, mixed): mixed)|null $sanitize
*/
public static function adapter(
array &$stored,
array &$writes,
?Closure $sanitize = null,
bool $customize = true
): ThemeOptionsAdapter {
$options = Mockery::mock(ThemeOptionsAdapter::class);
$options->allows('canCustomize')->andReturn($customize);
$options->allows('read')->andReturnUsing(static function () use (&$stored): array {
return $stored;
});
$options->allows('sanitize')
->andReturnUsing($sanitize ?? static fn (string $key, mixed $value): mixed => $value);
$options->allows('write')->andReturnUsing(
static function (array $set, array $unset) use (&$stored, &$writes): array {
$writes[] = [$set, $unset];
$stored = array_diff_key(array_merge($stored, $set), array_flip($unset));
return $stored;
}
);
return $options;
}
private function area(array &$stored, array &$writes): ThemeOptionsArea
{
return new ThemeOptionsArea(self::adapter($stored, $writes));
}
public function testAreaReadsThroughTheAdapter(): void
{
$stored = self::stored();
$writes = [];
$area = $this->area($stored, $writes);
self::assertSame(['theme_options', 'Theme-Optionen'], [$area->name(), $area->label()]);
self::assertSame(DiviFeature::ThemeOptionWrite, $area->feature());
self::assertSame(ThemeOptionBook::fromDivi($stored)->data(), $area->raw());
self::assertSame(ThemeOptionBook::fromDivi($stored)->rev(), $area->rev($stored));
}
/** Ruling W6: keys of the planned inventory are set, whitelist keys missing from it are deleted. */
public function testWriteSetsThePlannedKeysAndDeletesTheMissingOnes(): void
{
$stored = self::stored();
$writes = [];
$planned = ['heading_font' => 'Roboto', 'body_font' => 'Open Sans', 'body_font_size' => 18];
$saved = $this->area($stored, $writes)->write($planned, false);
self::assertCount(1, $writes);
self::assertSame(ThemeOptionBook::fromDivi($planned)->data(), $writes[0][0]);
self::assertSame(array_values(array_diff(ThemeOptionSpec::KEYS, array_keys($planned))), $writes[0][1]);
self::assertContains('body_font_height', $writes[0][1]);
self::assertSame(ThemeOptionBook::fromDivi($planned)->data(), $saved);
self::assertArrayNotHasKey('body_font_height', $stored);
}
/** Ruling W6: a restore (and a rollback) replaces the whole whitelist with the snapshot. */
public function testRestoreReplacesTheWholeWhitelistWithTheSnapshot(): void
{
$stored = ['body_font_size' => 20, 'tablet_body_font_size' => 18, 'gutter_width' => 2];
$writes = [];
$snapshot = ThemeOptionBook::fromDivi(self::stored())->data();
$saved = $this->area($stored, $writes)->restore($snapshot);
self::assertSame($snapshot, $saved);
self::assertSame($snapshot, ThemeOptionBook::fromDivi($stored)->data());
self::assertContains('tablet_body_font_size', $writes[0][1]);
self::assertContains('gutter_width', $writes[0][1]);
}
/** verify() tolerates only a number stored as another type (15 / "15" / 15.0), nothing else. */
public function testVerifyInsistsOnTheSameKeysAndValues(): void
{
$stored = self::stored();
$writes = [];
$area = $this->area($stored, $writes);
$expected = self::stored();
self::assertTrue($area->verify($expected, $expected));
self::assertTrue($area->verify($expected, ['body_font_size' => '16', 'body_font_height' => '1.8'] + $expected));
self::assertFalse($area->verify($expected, ['body_font_size' => 15] + $expected), 'changed value');
self::assertFalse($area->verify($expected, ['body_font' => 'open sans'] + $expected), 'fonts are exact');
$missing = $expected;
unset($missing['body_font_height']);
self::assertFalse($area->verify($expected, $missing), 'missing key');
self::assertFalse($area->verify($expected, $expected + ['gutter_width' => 3]), 'added key');
self::assertFalse($area->verify(['heading_font' => false], ['heading_font' => '']), 'false is not ""');
}
public function testDifferencesNameTheValuesDiviStoredAsAnotherType(): void
{
$stored = self::stored();
$writes = [];
$area = $this->area($stored, $writes);
$expected = self::stored();
self::assertSame([], $area->differences($expected, $expected));
self::assertSame(
[sprintf(
'Divi hat die Theme-Option „%s“ als „16“ statt 16 gespeichert.',
ThemeOptionSpec::definition('body_font_size')['label']
)],
$area->differences($expected, ['body_font_size' => '16'] + $expected)
);
}
}