feat: lit/divi-presets-set – Presets anlegen und ändern mit Probelauf und Snapshot
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
341782a34f
commit
b3c3e69a53
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignRights;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class DesignRightsTest extends TestCase
|
||||
{
|
||||
public function testPresetsNeedEditThemeOptionsAndTheVisualBuilder(): void
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true, false);
|
||||
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
|
||||
|
||||
self::assertNull(DesignRights::presets($writer));
|
||||
$refusal = self::assertToolError(DesignRights::presets($writer), 'forbidden');
|
||||
self::assertStringContainsString('Visual Builder', $refusal['message']);
|
||||
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
$refusal = self::assertToolError(DesignRights::presets($writer), 'forbidden');
|
||||
self::assertStringContainsString('edit_theme_options', $refusal['message']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\Compose\StoredNodeGuard;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetBook;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetScope;
|
||||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||||
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||||
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class DiviPresetsSetAbilityTest extends TestCase
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $stored;
|
||||
|
||||
private SnapshotStore $snapshots;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->stored = PresetBookTest::stored();
|
||||
$this->snapshots = Mockery::mock(SnapshotStore::class);
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
}
|
||||
|
||||
private function ability(bool $unfiltered = true, ?GlobalUsages $usages = null, ?DiviPageLoader $loader = null, ?DesignSystemWriter $writer = null): DiviPresetsSetAbility
|
||||
{
|
||||
$writer ??= $this->savingWriter();
|
||||
$usages ??= Mockery::mock(GlobalUsages::class);
|
||||
$usages->allows('reportFor')->andReturn(new UsageReport(0, [], 0, 0, [], 0, false));
|
||||
return $this->build($writer, $usages, $loader, $unfiltered);
|
||||
}
|
||||
|
||||
private function savingWriter(): DesignSystemWriter
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$writer->allows('savePresets')->andReturnUsing(function (array $rest, bool $mayRemove): array {
|
||||
$data = ['module' => [], 'group' => []];
|
||||
foreach ($rest as $kind => $records) {
|
||||
foreach ($records as $record) {
|
||||
$first = $record['items'][0] ?? null;
|
||||
$name = $kind === 'group' ? ($first['groupName'] ?? 'x') : ($first['moduleName'] ?? 'x');
|
||||
$data[$kind][$name] = ['default' => $record['default'], 'items' => array_column($record['items'], null, 'id')];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->stored = $data;
|
||||
});
|
||||
|
||||
return $writer;
|
||||
}
|
||||
|
||||
private function build(DesignSystemWriter $writer, GlobalUsages $usages, ?DiviPageLoader $loader, bool $unfiltered): DiviPresetsSetAbility
|
||||
{
|
||||
$composer = static fn (): PresetComposer => new PresetComposer(
|
||||
ComposeFixture::composer($unfiltered),
|
||||
new StoredNodeGuard(ComposeFixture::cleaner($unfiltered), ComposeFixture::references(),
|
||||
ComposeFixture::sanitizer($unfiltered)),
|
||||
static fn (string $module, array $attrs): array => ['all' => array_map(
|
||||
static fn (mixed $v): mixed => is_array($v) ? array_diff_key($v, ['innerContent' => 1]) : $v,
|
||||
$attrs
|
||||
), 'style' => [], 'render' => []],
|
||||
static fn (string $module): array => ['module.decoration.spacing' => 'divi/spacing'],
|
||||
'5.15.1',
|
||||
static fn (): int => 1
|
||||
);
|
||||
|
||||
return new DiviPresetsSetAbility(
|
||||
$writer,
|
||||
new DesignChange($this->snapshots, new FakeChangeLog(5)),
|
||||
$usages,
|
||||
$loader ?? new DiviPageLoader(DiviFixtures::schema()),
|
||||
$composer
|
||||
);
|
||||
}
|
||||
|
||||
private static function newButton(array $extra = []): array
|
||||
{
|
||||
return ['items' => [$extra + ['module' => 'button', 'name' => 'CTA dunkel',
|
||||
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#111111']]]]]];
|
||||
}
|
||||
|
||||
public function testDryRunThenConfirmCreatesThePreset(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
|
||||
$dry = self::assertToolError($ability->execute(self::newButton()), 'confirm_required');
|
||||
self::assertSame('added', $dry['diff'][0]['action']);
|
||||
self::assertStringContainsString('Presets: 1 angelegt.', $dry['message']);
|
||||
|
||||
$this->snapshots->expects('save')->andReturn('presets-1');
|
||||
$this->snapshots->expects('attachLog');
|
||||
$result = $ability->execute(self::newButton() + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('presets-1', $result['snapshot_id']);
|
||||
$names = array_column(PresetBook::fromDivi($this->stored)->items(new PresetScope('module', 'divi/button')), 'name');
|
||||
self::assertContains('CTA dunkel', $names);
|
||||
}
|
||||
|
||||
public function testExistingNameIsRefusedForANewPreset(): void
|
||||
{
|
||||
$result = self::assertToolError($this->ability()->execute(['items' => [
|
||||
['module' => 'button', 'name' => 'CTA primär'],
|
||||
]]), 'validation_failed');
|
||||
|
||||
self::assertStringContainsString('aaaaaaaaaa', $result['fields']['items[0].name']);
|
||||
}
|
||||
|
||||
public function testChangingAndMakingDefault(): void
|
||||
{
|
||||
$dry = self::assertToolError($this->ability()->execute(['items' => [[
|
||||
'module' => 'button', 'preset' => 'CTA primär', 'make_default' => true,
|
||||
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#222222']]],
|
||||
]]]), 'confirm_required');
|
||||
|
||||
self::assertSame(['changed', 'default_set'], array_column($dry['diff'], 'action'));
|
||||
}
|
||||
|
||||
public function testUnknownPresetAndBadItemsAreCollected(): void
|
||||
{
|
||||
$result = self::assertToolError($this->ability()->execute(['items' => [
|
||||
['module' => 'button', 'preset' => 'Fehlt'],
|
||||
['name' => 'ohne Modul'],
|
||||
]]), 'validation_failed');
|
||||
|
||||
self::assertArrayHasKey('items[0].preset', $result['fields']);
|
||||
self::assertArrayHasKey('items[1]', $result['fields']);
|
||||
}
|
||||
|
||||
public function testEditorWithoutUnfilteredHtmlIsRefusedForRawCss(): void
|
||||
{
|
||||
$result = $this->ability(false)->execute(['items' => [[
|
||||
'module' => 'button', 'name' => 'Mit CSS',
|
||||
'attrs' => ['css' => ['desktop' => ['mainElement' => 'color:red']]],
|
||||
]]]);
|
||||
|
||||
self::assertNotTrue(is_array($result) && ($result['ok'] ?? false));
|
||||
self::assertContains(Errors::codeOf($result), ['validation_failed', 'invalid_attr_path']);
|
||||
}
|
||||
|
||||
public function testFromNodeReadsThePage(): void
|
||||
{
|
||||
$content = '<!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
|
||||
. '<!-- wp:divi/button {"button":{"decoration":{"background":{"desktop":{"value":{"color":"#abcdef"}}}}}} /-->'
|
||||
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section -->';
|
||||
DiviFixtures::givenPost($content);
|
||||
$loader = new DiviPageLoader(DiviFixtures::schema());
|
||||
|
||||
$dry = self::assertToolError($this->ability(true, null, $loader)->execute(['items' => [[
|
||||
'module' => 'button', 'name' => 'Aus Seite', 'from_node' => ['post_id' => 7, 'id' => 's0.r0.c0.m0'],
|
||||
]]]), 'confirm_required');
|
||||
|
||||
self::assertSame('added', $dry['diff'][0]['action']);
|
||||
}
|
||||
|
||||
public function testNamesAreCleanedAndLimited(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
$dry = self::assertToolError($ability->execute(self::newButton(['name' => '<b>Fett</b> Name'])), 'confirm_required');
|
||||
|
||||
$this->snapshots->expects('save')->andReturn('presets-1');
|
||||
$this->snapshots->expects('attachLog');
|
||||
$res = $ability->execute(self::newButton(['name' => '<b>Fett</b> Name']) + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
|
||||
self::assertTrue($res['ok']);
|
||||
$names = array_column(PresetBook::fromDivi($this->stored)->items(new PresetScope('module', 'divi/button')), 'name');
|
||||
self::assertContains('Fett Name', $names);
|
||||
self::assertNotContains('<b>Fett</b> Name', $names);
|
||||
|
||||
$long = self::assertToolError($this->ability()->execute(self::newButton(['name' => str_repeat('n', 101)])), 'validation_failed');
|
||||
self::assertStringContainsString('1–100 Zeichen', $long['fields']['items[0].name']);
|
||||
$empty = self::assertToolError($this->ability()->execute(['items' => [
|
||||
['module' => 'button', 'preset' => 'CTA primär', 'rename' => '<i></i>'],
|
||||
]]), 'validation_failed');
|
||||
self::assertStringContainsString('1–100 Zeichen', $empty['fields']['items[0].rename']);
|
||||
}
|
||||
|
||||
public function testNothingIsWrittenWhenOneItemHasErrors(): void
|
||||
{
|
||||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||
$writer->allows('presets')->andReturnUsing(fn (): array => $this->stored);
|
||||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||||
$writer->shouldNotReceive('savePresets');
|
||||
$this->snapshots->shouldNotReceive('save');
|
||||
$ability = $this->ability(true, null, null, $writer);
|
||||
|
||||
$result = $ability->execute(['items' => [
|
||||
['module' => 'button', 'name' => 'Gut'],
|
||||
['module' => 'button', 'name' => 'Schlecht', 'attrs' => ['button.innerContent' => ['desktop' => ['text' => 'Hallo']]]],
|
||||
], 'confirm' => true, 'design_rev' => 'egal']);
|
||||
|
||||
self::assertContains(Errors::codeOf($result), ['validation_failed', 'invalid_attr_path']);
|
||||
self::assertNotEmpty(array_filter(
|
||||
array_keys(Errors::fieldsOf($result)),
|
||||
static fn (string $key): bool => str_starts_with($key, 'items[1]')
|
||||
));
|
||||
}
|
||||
|
||||
public function testNeedsEditThemeOptionsAndTheVisualBuilder(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(false);
|
||||
|
||||
self::assertToolError($this->ability()->permission(self::newButton()), 'forbidden');
|
||||
}
|
||||
|
||||
public function testSchemaAndMeta(): void
|
||||
{
|
||||
$ability = $this->ability();
|
||||
|
||||
self::assertSame('lit/divi-presets-set', $ability->name());
|
||||
self::assertFalse($ability->isDirect());
|
||||
self::assertSame(DiviPresetsSetAbility::MAX_ITEMS, $ability->inputSchema()['properties']['items']['maxItems']);
|
||||
self::assertSame(['diff', 'design_rev'], array_slice($ability->outputSchema()['required'], -2));
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,17 @@ final class PresetDiffTest extends TestCase
|
||||
'action' => 'default_cleared'],
|
||||
], PresetDiff::between($before, $after));
|
||||
}
|
||||
|
||||
public function testSummaryCountsTheActions(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'Presets: 1 angelegt, 1 geändert, 1 gelöscht, Standard 1× gesetzt, 1× entfernt.',
|
||||
PresetDiff::summary([
|
||||
['action' => 'added'], ['action' => 'changed'], ['action' => 'removed'],
|
||||
['action' => 'default_set'], ['action' => 'default_cleared'],
|
||||
])
|
||||
);
|
||||
self::assertSame('Presets: 2 angelegt.', PresetDiff::summary([['action' => 'added'], ['action' => 'added']]));
|
||||
self::assertSame('Presets: keine Änderung.', PresetDiff::summary([]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ final class DiviIntegrationTest extends TestCase
|
||||
'lit/divi-node-copy',
|
||||
'lit/divi-dynamic-sources',
|
||||
'lit/divi-preset-get',
|
||||
'lit/divi-presets-set',
|
||||
];
|
||||
|
||||
public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void
|
||||
|
||||
Reference in New Issue
Block a user