Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
233 lines
10 KiB
PHP
233 lines
10 KiB
PHP
<?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));
|
||
}
|
||
}
|