feat: lit/divi-theme-options-get liest Wert, wirksamen Wert, Standard und Bereich je Theme-Option
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
1bc8ce65b7
commit
122548db12
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lit/divi-theme-options-get (spec §8.1, rulings W2, W3): the 20 whitelisted Divi theme options – per key the stored
|
||||||
|
* value (null = unset), the value in effect (an unset tablet/phone size inherits the larger size), Divi's default,
|
||||||
|
* type, range or choices – and the design_rev of the area. Reading needs what lit/divi-design-system needs.
|
||||||
|
*/
|
||||||
|
final class DiviThemeOptionsGetAbility implements Ability, RequiresDiviFeatures
|
||||||
|
{
|
||||||
|
/** Fields of ThemeOptionSpec::definition() passed through as they are. */
|
||||||
|
private const RULES = ['min', 'max', 'step', 'choices', 'inherits'];
|
||||||
|
|
||||||
|
private const NOTES = [
|
||||||
|
'Ändern mit lit/divi-theme-options-update: set {Schlüssel: Wert}; null setzt auf den Divi-Standard zurück.',
|
||||||
|
'Ohne eigenen Wert erben Tablet- und Smartphone-Größen die nächstgrößere Größe (inherits).',
|
||||||
|
'Die fünf Hauptfarben ändert lit/divi-colors-set; Buttons, Logo und SEO bleiben im Divi-Customizer.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(private ThemeOptionsAdapter $options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'lit/divi-theme-options-get';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Divi-Theme-Optionen lesen';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Liest Divis Theme-Optionen für Schriften, Schriftgrößen und Layout: je Schlüssel den gespeicherten '
|
||||||
|
. 'Wert (null = nicht gesetzt), den wirksamen Wert, den Divi-Standard, Typ und Bereich oder Auswahl, dazu '
|
||||||
|
. 'die design_rev. keys grenzt auf einzelne Schlüssel ein. Ändern mit lit/divi-theme-options-update.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'keys' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => ['type' => 'string', 'enum' => ThemeOptionSpec::KEYS],
|
||||||
|
'description' => 'Nur diese Theme-Optionen liefern (Standard: alle).',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'additionalProperties' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputSchema(): ?array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'options' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'key' => ['type' => 'string'],
|
||||||
|
'label' => ['type' => 'string'],
|
||||||
|
'type' => ['type' => 'string'],
|
||||||
|
'is_default' => ['type' => 'boolean'],
|
||||||
|
],
|
||||||
|
'required' => ['key', 'label', 'type', 'value', 'effective', 'default', 'is_default'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'design_rev' => ['type' => 'string'],
|
||||||
|
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
],
|
||||||
|
'required' => ['options', 'design_rev', 'notes'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function annotations(): array
|
||||||
|
{
|
||||||
|
return Annotations::read();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirect(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads et_divi and the Customizer table (ThemeOptionsAdapter, ThemeOptionSpec mirror). */
|
||||||
|
public function diviFeatures(array $input): array
|
||||||
|
{
|
||||||
|
return [DiviFeature::ThemeOptionRead];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permission(array $input): bool|WP_Error
|
||||||
|
{
|
||||||
|
return self::rights() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(array $input): mixed
|
||||||
|
{
|
||||||
|
$refusal = self::rights();
|
||||||
|
if ($refusal !== null) {
|
||||||
|
return $refusal;
|
||||||
|
}
|
||||||
|
$keys = self::keys($input);
|
||||||
|
if ($keys instanceof WP_Error) {
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
$book = ThemeOptionBook::fromDivi($this->options->read());
|
||||||
|
$rows = [];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$definition = ThemeOptionSpec::definition($key);
|
||||||
|
$rows[] = [
|
||||||
|
'key' => $key,
|
||||||
|
'label' => $definition['label'],
|
||||||
|
'type' => $definition['type'],
|
||||||
|
'value' => $book->get($key),
|
||||||
|
'effective' => ThemeOptionSpec::effective($key, $book->data()),
|
||||||
|
'default' => ThemeOptionSpec::effective($key, $book->without($key)->data()),
|
||||||
|
'is_default' => ! $book->has($key),
|
||||||
|
] + array_intersect_key($definition, array_flip(self::RULES));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['options' => $rows, 'design_rev' => $book->rev(), 'notes' => self::NOTES];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like lit/divi-design-system: whoever edits content may read the design. */
|
||||||
|
private static function rights(): ?WP_Error
|
||||||
|
{
|
||||||
|
return current_user_can('edit_posts') ? null : Errors::forbidden('Keine Berechtigung, Inhalte zu bearbeiten.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
* @return list<string>|WP_Error The requested keys in ThemeOptionSpec::KEYS order (all without "keys").
|
||||||
|
*/
|
||||||
|
private static function keys(array $input): array|WP_Error
|
||||||
|
{
|
||||||
|
$keys = $input['keys'] ?? ThemeOptionSpec::KEYS;
|
||||||
|
if (! is_array($keys) || ! array_is_list($keys)) {
|
||||||
|
return Errors::invalidInput('keys muss eine Liste sein.', ['keys' => 'Liste von Theme-Optionen']);
|
||||||
|
}
|
||||||
|
$errors = [];
|
||||||
|
foreach ($keys as $index => $key) {
|
||||||
|
if (! is_string($key) || ! ThemeOptionSpec::isKey($key)) {
|
||||||
|
$errors[sprintf('keys[%d]', $index)] = 'unbekannte Theme-Option; erlaubt sind die Schlüssel aus '
|
||||||
|
. 'lit/divi-theme-options-get ohne keys';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($errors !== []) {
|
||||||
|
return Errors::invalidInput('Unbekannte Theme-Optionen.', $errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_intersect(ThemeOptionSpec::KEYS, $keys));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\DiviThemeOptionsGetAbility;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionBook;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionSpec;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
use Mockery;
|
||||||
|
|
||||||
|
final class DiviThemeOptionsGetAbilityTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ability(): DiviThemeOptionsGetAbility
|
||||||
|
{
|
||||||
|
$stored = ThemeOptionsAreaTest::stored();
|
||||||
|
$writes = [];
|
||||||
|
|
||||||
|
return new DiviThemeOptionsGetAbility(ThemeOptionsAreaTest::adapter($stored, $writes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $result
|
||||||
|
* @return array<string, array<string, mixed>> key => row
|
||||||
|
*/
|
||||||
|
private static function byKey(array $result): array
|
||||||
|
{
|
||||||
|
return array_column($result['options'], null, 'key');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSchemaAndMeta(): void
|
||||||
|
{
|
||||||
|
$ability = self::ability();
|
||||||
|
$schema = $ability->inputSchema();
|
||||||
|
|
||||||
|
self::assertSame('lit/divi-theme-options-get', $ability->name());
|
||||||
|
self::assertFalse($ability->isDirect());
|
||||||
|
self::assertSame(Annotations::read(), $ability->annotations());
|
||||||
|
self::assertSame([DiviFeature::ThemeOptionRead], $ability->diviFeatures([]));
|
||||||
|
self::assertSame(ThemeOptionSpec::KEYS, $schema['properties']['keys']['items']['enum']);
|
||||||
|
self::assertFalse($schema['additionalProperties']);
|
||||||
|
self::assertSame(['options', 'design_rev', 'notes'], $ability->outputSchema()['required']);
|
||||||
|
self::assertStringContainsString('lit/divi-theme-options-update', $ability->description());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reading needs what lit/divi-design-system needs (edit_posts) – checked before Divi is asked. */
|
||||||
|
public function testNeedsEditPosts(): void
|
||||||
|
{
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
$options = Mockery::mock(ThemeOptionsAdapter::class);
|
||||||
|
$options->shouldNotReceive('read');
|
||||||
|
$ability = new DiviThemeOptionsGetAbility($options);
|
||||||
|
|
||||||
|
self::assertToolError($ability->permission([]), 'forbidden');
|
||||||
|
self::assertToolError($ability->execute([]), 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListsEveryOptionWithValueEffectiveValueAndDefault(): void
|
||||||
|
{
|
||||||
|
$result = self::ability()->execute([]);
|
||||||
|
|
||||||
|
self::assertSame(ThemeOptionSpec::KEYS, array_column($result['options'], 'key'));
|
||||||
|
self::assertSame(ThemeOptionBook::fromDivi(ThemeOptionsAreaTest::stored())->rev(), $result['design_rev']);
|
||||||
|
$rows = self::byKey($result);
|
||||||
|
$size = ThemeOptionSpec::definition('body_font_size');
|
||||||
|
$expected = ['key' => 'body_font_size', 'label' => $size['label'], 'type' => 'int', 'value' => 16,
|
||||||
|
'effective' => 16, 'default' => ThemeOptionSpec::effective('body_font_size', []), 'is_default' => false,
|
||||||
|
'min' => $size['min'], 'max' => $size['max'], 'step' => $size['step']];
|
||||||
|
$actual = $rows['body_font_size'];
|
||||||
|
ksort($expected);
|
||||||
|
ksort($actual);
|
||||||
|
self::assertSame($expected, $actual);
|
||||||
|
self::assertSame('Open Sans', $rows['body_font']['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling W3: an unset tablet size inherits the desktop size – as effective value and as its default. */
|
||||||
|
public function testUnsetSizesInheritTheLargerSize(): void
|
||||||
|
{
|
||||||
|
$tablet = self::byKey(self::ability()->execute([]))['tablet_body_font_size'];
|
||||||
|
|
||||||
|
self::assertNull($tablet['value']);
|
||||||
|
self::assertTrue($tablet['is_default']);
|
||||||
|
self::assertSame(16, $tablet['effective']);
|
||||||
|
self::assertSame(16, $tablet['default']);
|
||||||
|
self::assertSame('body_font_size', $tablet['inherits']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChoicesAreListed(): void
|
||||||
|
{
|
||||||
|
$rows = self::byKey(self::ability()->execute([]));
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
ThemeOptionSpec::definition('body_header_style')['choices'],
|
||||||
|
$rows['body_header_style']['choices']
|
||||||
|
);
|
||||||
|
self::assertArrayNotHasKey('choices', $rows['body_font']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKeysNarrowTheAnswerInTheFixedOrder(): void
|
||||||
|
{
|
||||||
|
$result = self::ability()->execute(['keys' => ['row_padding', 'body_font']]);
|
||||||
|
|
||||||
|
self::assertSame(['body_font', 'row_padding'], array_column($result['options'], 'key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownKeysAreRefused(): void
|
||||||
|
{
|
||||||
|
$error = self::ability()->execute(['keys' => ['accent_color', 'body_font', 5]]);
|
||||||
|
|
||||||
|
self::assertToolError($error, 'validation_failed');
|
||||||
|
self::assertSame(['keys[0]', 'keys[2]'], array_keys(self::errorFields($error)));
|
||||||
|
self::assertToolError(self::ability()->execute(['keys' => 'body_font']), 'validation_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** G3, A8: the main colors are not theme options here; the notes point to lit/divi-colors-set. */
|
||||||
|
public function testNotesPointToTheColorTool(): void
|
||||||
|
{
|
||||||
|
$notes = implode(' ', self::ability()->execute([])['notes']);
|
||||||
|
|
||||||
|
self::assertStringContainsString('lit/divi-colors-set', $notes);
|
||||||
|
self::assertStringContainsString('null', $notes);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user