Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
193 lines
7.8 KiB
PHP
193 lines
7.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||
|
||
use Brain\Monkey\Functions;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignChange;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetUsageQuery;
|
||
use LIT\MCP\Integrations\Divi\Design\SnapshotStore;
|
||
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
|
||
use LIT\MCP\Integrations\Divi\Library\UsageReport;
|
||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||
use LIT\MCP\Tests\Support\LockTable;
|
||
use LIT\MCP\Tests\Unit\TestCase;
|
||
use Mockery;
|
||
use ReflectionProperty;
|
||
|
||
final class DiviPresetsDeleteAbilityTest extends TestCase
|
||
{
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
// DesignLock (final review I-5): the lock is always free here.
|
||
LockTable::install();
|
||
}
|
||
|
||
private function ability(
|
||
UsageReport $report,
|
||
?DesignSystemWriter $writer = null,
|
||
?SnapshotStore $snapshots = null,
|
||
bool $unfiltered = true,
|
||
array $stored = []
|
||
): DiviPresetsDeleteAbility {
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
if ($writer === null) {
|
||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||
$writer->allows('refresh');
|
||
$writer->allows('presets')->andReturn($stored === [] ? PresetBookTest::stored() : $stored);
|
||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||
}
|
||
$usages = Mockery::mock(GlobalUsages::class);
|
||
$usages->allows('reportFor')->andReturn($report);
|
||
|
||
return new DiviPresetsDeleteAbility(
|
||
$writer,
|
||
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), new FakeChangeLog(3)),
|
||
$usages,
|
||
static fn (): bool => $unfiltered
|
||
);
|
||
}
|
||
|
||
private static function unused(): UsageReport
|
||
{
|
||
return new UsageReport(0, [], 0, 0, [], 0, false);
|
||
}
|
||
|
||
public function testUnusedPresetIsDeletedAfterConfirm(): void
|
||
{
|
||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||
$writer->allows('refresh');
|
||
$writer->allows('presets')->andReturn(PresetBookTest::stored());
|
||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||
$writer->expects('savePresets')
|
||
->with(Mockery::on(static function (array $rest): bool {
|
||
$ids = array_column($rest['module'][0]['items'], 'id');
|
||
|
||
return $ids === ['aaaaaaaaaa', 'cccccccccc'];
|
||
}), true)
|
||
->andReturnUsing(static function (): array {
|
||
$stored = PresetBookTest::stored();
|
||
unset($stored['module']['divi/button']['items']['bbbbbbbbbb']);
|
||
|
||
return $stored;
|
||
});
|
||
$snapshots = Mockery::mock(SnapshotStore::class);
|
||
$snapshots->expects('save')->andReturn('presets-9');
|
||
$snapshots->expects('attachLog');
|
||
$ability = $this->ability(self::unused(), $writer, $snapshots);
|
||
$input = ['items' => [['module' => 'button', 'preset' => 'bbbbbbbbbb']]];
|
||
|
||
$dry = self::assertToolError($ability->execute($input), 'confirm_required');
|
||
self::assertSame('removed', $dry['diff'][0]['action']);
|
||
|
||
$result = $ability->execute($input + ['confirm' => true, 'design_rev' => $dry['design_rev']]);
|
||
self::assertSame('presets-9', $result['snapshot_id']);
|
||
}
|
||
|
||
public function testPresetInUseIsRefused(): void
|
||
{
|
||
$used = new UsageReport(2, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']],
|
||
1, 1, [], 0, false);
|
||
|
||
$error = self::assertToolError(
|
||
$this->ability($used)->execute(['items' => [['module' => 'button', 'preset' => 'CTA primär']]]),
|
||
'in_use'
|
||
);
|
||
|
||
self::assertSame(3, $error['usages'][0]['post_id']);
|
||
self::assertStringContainsString('lit/divi-design-detach', $error['hint']);
|
||
self::assertStringNotContainsString('4b', $error['hint'], 'the tool exists now');
|
||
}
|
||
|
||
public function testNestedUseInAnotherPresetCounts(): void
|
||
{
|
||
$error = self::assertToolError(
|
||
$this->ability(self::unused())->execute(['items' => [['group' => 'font-body', 'preset' => 'Lesetext']]]),
|
||
'in_use'
|
||
);
|
||
|
||
self::assertSame('preset', $error['usages'][0]['type']);
|
||
self::assertSame('dddddddddd', $error['usages'][0]['preset_id']);
|
||
}
|
||
|
||
public function testDeletingTheDefaultClearsIt(): void
|
||
{
|
||
$dry = self::assertToolError(
|
||
$this->ability(self::unused())->execute(['items' => [['module' => 'text', 'preset' => 'Fließtext']]]),
|
||
'confirm_required'
|
||
);
|
||
|
||
self::assertSame(['removed', 'default_cleared'], array_column($dry['diff'], 'action'));
|
||
}
|
||
|
||
/** Final review I-2: deleting saves the whole inventory – other presets' HTML/CSS would be stripped. */
|
||
public function testEditorIsRefusedWhenOtherPresetsCarryMarkup(): void
|
||
{
|
||
$input = ['items' => [['module' => 'button', 'preset' => 'cccccccccc']]];
|
||
$markup = PresetBookTest::storedWithMarkup();
|
||
|
||
$editor = self::assertToolError(
|
||
$this->ability(self::unused(), null, null, false, $markup)->execute($input),
|
||
'forbidden'
|
||
);
|
||
self::assertStringContainsString('unfiltered_html', $editor['message']);
|
||
|
||
self::assertToolError($this->ability(self::unused(), null, null, true, $markup)->execute($input), 'confirm_required');
|
||
self::assertToolError($this->ability(self::unused(), null, null, false)->execute($input), 'confirm_required');
|
||
}
|
||
|
||
/** @return list<bool> The default flags of a PresetUsageQuery's targets. */
|
||
private static function defaultFlags(PresetUsageQuery $query): array
|
||
{
|
||
$targets = (new ReflectionProperty(PresetUsageQuery::class, 'targets'))->getValue($query);
|
||
|
||
return array_column($targets, 'default');
|
||
}
|
||
|
||
/** Final review I-6 (spec §6.4): implicit use of a default preset does not block; the dry run lists it. */
|
||
public function testImplicitUseOfADefaultPresetIsListedNotBlocking(): void
|
||
{
|
||
$used = new UsageReport(1, [['post_id' => 3, 'title' => 'Start', 'status' => 'publish', 'type' => 'page']],
|
||
0, 1, [], 0, false);
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
$writer = Mockery::mock(DesignSystemWriter::class);
|
||
$writer->allows('refresh');
|
||
$writer->allows('presets')->andReturn(PresetBookTest::stored());
|
||
$writer->allows('canUseVisualBuilder')->andReturn(true);
|
||
$usages = Mockery::mock(GlobalUsages::class);
|
||
$usages->expects('reportFor')
|
||
->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [false]), false)
|
||
->andReturn(self::unused());
|
||
$usages->expects('reportFor')
|
||
->with(Mockery::on(static fn (PresetUsageQuery $q): bool => self::defaultFlags($q) === [true]))
|
||
->andReturn($used);
|
||
$ability = new DiviPresetsDeleteAbility(
|
||
$writer,
|
||
new DesignChange(Mockery::mock(SnapshotStore::class), FakeChangeLog::forbidden()),
|
||
$usages,
|
||
static fn (): bool => true
|
||
);
|
||
|
||
$dry = self::assertToolError(
|
||
$ability->execute(['items' => [['module' => 'text', 'preset' => 'Fließtext']]]),
|
||
'confirm_required'
|
||
);
|
||
|
||
self::assertSame(3, $dry['affected'][0]['post_id']);
|
||
self::assertSame(1, $dry['affected_count']);
|
||
self::assertContains('Module ohne eigenes Preset verwenden danach das Divi-Standardaussehen.', $dry['warnings']);
|
||
}
|
||
|
||
public function testUnknownPreset(): void
|
||
{
|
||
self::assertToolError(
|
||
$this->ability(self::unused())->execute(['items' => [['module' => 'button', 'preset' => 'Fehlt']]]),
|
||
'validation_failed'
|
||
);
|
||
}
|
||
}
|