Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Design/DiviPresetsSetAbilityTest.php
T

257 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 testNameAndRenameAreExclusive(): void
{
$ability = $this->ability();
$new = self::assertToolError($ability->execute(['items' => [
['module' => 'button', 'name' => 'Neu', 'rename' => '<b>x</b>'],
]]), 'validation_failed');
self::assertStringContainsString('nur zusammen mit preset', $new['fields']['items[0].rename']);
$change = self::assertToolError($ability->execute(['items' => [
['module' => 'button', 'preset' => 'CTA primär', 'name' => 'CTA primär'],
]]), 'validation_failed');
self::assertStringContainsString('rename', $change['fields']['items[0].name']);
}
public function testRenameToAnotherPresetsNameIsRefused(): void
{
$result = self::assertToolError($this->ability()->execute(['items' => [
['module' => 'button', 'preset' => 'CTA primär', 'rename' => 'Doppelt'],
]]), 'validation_failed');
self::assertStringContainsString('bbbbbbbbbb', $result['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',
'attrs' => ['button.decoration.background' => ['desktop' => ['color' => '#333333']]]],
['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));
}
}