feat: ThemeOptionsRestorePlanner – Snapshot theme_options ersetzt die Whitelist, nennt zurückgesetzte Optionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
449ec5c33f
commit
2e3b740260
@@ -0,0 +1,71 @@
|
|||||||
|
<?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 theme options (rulings W6, W11): the snapshot replaces the whole whitelist – keys it
|
||||||
|
* lacks are deleted and return to Divi's default (ThemeOptionDiff "reset" rows, named in a warning). The rights and
|
||||||
|
* Divi features are those of lit/divi-theme-options-update.
|
||||||
|
*/
|
||||||
|
final class ThemeOptionsRestorePlanner implements RestorePlanner
|
||||||
|
{
|
||||||
|
private const RESET = 'Theme-Optionen, die im Snapshot nicht gesetzt sind, kehren zum Divi-Standard zurück (%s).';
|
||||||
|
|
||||||
|
public function __construct(private DesignSystemWriter $writer, private ThemeOptionsAdapter $options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function area(): string
|
||||||
|
{
|
||||||
|
return 'theme_options';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function designArea(): DesignArea
|
||||||
|
{
|
||||||
|
return new ThemeOptionsArea($this->options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rights(): ?WP_Error
|
||||||
|
{
|
||||||
|
return DesignRights::themeOptions($this->writer, $this->options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like lit/divi-theme-options-update: reads et_divi (ThemeOptionRead), writes it (ThemeOptionWrite). */
|
||||||
|
public function features(): array
|
||||||
|
{
|
||||||
|
return [DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function plan(array $current, array $snapshot, string $snapshotId): DesignPlan|WP_Error
|
||||||
|
{
|
||||||
|
// A stored false is "not set" (W3), as in lit/divi-theme-options-update: no noise rows for it.
|
||||||
|
$after = ThemeOptionBook::fromDivi($snapshot)->withoutFalse();
|
||||||
|
$diff = ThemeOptionDiff::between(ThemeOptionBook::fromDivi($current)->withoutFalse(), $after);
|
||||||
|
$reset = [];
|
||||||
|
foreach ($diff as $row) {
|
||||||
|
if ($row['action'] === 'reset') {
|
||||||
|
$reset[] = '„' . $row['label'] . '“';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$warnings = $diff === [] ? [] : [ThemeOptionsArea::SITE_WIDE];
|
||||||
|
if ($reset !== []) {
|
||||||
|
$warnings[] = sprintf(self::RESET, implode(', ', $reset));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DesignPlan(
|
||||||
|
$after->data(),
|
||||||
|
$diff,
|
||||||
|
sprintf('Snapshot %s wiederherstellen – %s', $snapshotId, ThemeOptionDiff::summary($diff)),
|
||||||
|
null,
|
||||||
|
$warnings,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?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\ThemeOptionBook;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionDiff;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsArea;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionSpec;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsRestorePlanner;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class ThemeOptionsRestorePlannerTest extends TestCase
|
||||||
|
{
|
||||||
|
public const SNAPSHOT_ID = 'theme_options-20261016T100000Z-aaaaaa';
|
||||||
|
|
||||||
|
/** @return array<string, int|float|string|false> The options now: larger text, a gutter set after the snapshot. */
|
||||||
|
public static function current(): array
|
||||||
|
{
|
||||||
|
return ['body_font_size' => 18, 'gutter_width' => 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, int|float|string|false> The snapshot: body font and text size 16, no gutter. */
|
||||||
|
public static function snapshot(): array
|
||||||
|
{
|
||||||
|
return ['body_font' => 'Open Sans', 'body_font_size' => 16];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function planner(bool $customize = true): ThemeOptionsRestorePlanner
|
||||||
|
{
|
||||||
|
$stored = [];
|
||||||
|
$writes = [];
|
||||||
|
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||||
|
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||||
|
|
||||||
|
return new ThemeOptionsRestorePlanner(
|
||||||
|
$writer,
|
||||||
|
ThemeOptionsAreaTest::adapter($stored, $writes, null, $customize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAreaFeaturesAndDesignArea(): void
|
||||||
|
{
|
||||||
|
$planner = self::planner();
|
||||||
|
|
||||||
|
self::assertSame('theme_options', $planner->area());
|
||||||
|
self::assertSame([DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite], $planner->features());
|
||||||
|
self::assertInstanceOf(ThemeOptionsArea::class, $planner->designArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling W11: the rights of lit/divi-theme-options-update. */
|
||||||
|
public function testNeedsTheThemeOptionRights(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
self::assertNull(self::planner()->rights());
|
||||||
|
self::assertToolError(self::planner(false)->rights(), 'forbidden');
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
self::assertToolError(self::planner()->rights(), 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling W6: the snapshot replaces the whole whitelist – keys it lacks return to Divi's default. */
|
||||||
|
public function testTheSnapshotReplacesEveryOptionAndNamesTheResetOnes(): void
|
||||||
|
{
|
||||||
|
$plan = self::planner()->plan(self::current(), self::snapshot(), self::SNAPSHOT_ID);
|
||||||
|
|
||||||
|
self::assertInstanceOf(DesignPlan::class, $plan);
|
||||||
|
self::assertTrue($plan->mayRemove);
|
||||||
|
self::assertNull($plan->usages);
|
||||||
|
self::assertSame(ThemeOptionBook::fromDivi(self::snapshot())->data(), $plan->raw);
|
||||||
|
self::assertSame(
|
||||||
|
[['body_font', 'added'], ['body_font_size', 'changed'], ['gutter_width', 'reset']],
|
||||||
|
array_map(static fn (array $row): array => [$row['key'], $row['action']], $plan->diff)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
'Snapshot ' . self::SNAPSHOT_ID . ' wiederherstellen – ' . ThemeOptionDiff::summary($plan->diff),
|
||||||
|
$plan->summary
|
||||||
|
);
|
||||||
|
self::assertSame([
|
||||||
|
ThemeOptionsArea::SITE_WIDE,
|
||||||
|
sprintf(
|
||||||
|
'Theme-Optionen, die im Snapshot nicht gesetzt sind, kehren zum Divi-Standard zurück („%s“).',
|
||||||
|
ThemeOptionSpec::definition('gutter_width')['label']
|
||||||
|
),
|
||||||
|
], $plan->warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheSameOptionsAreNoChange(): 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user