feat: lit/divi-presets-assign – Presets site-weit zuweisen mit Probelauf und höchstens 50 Seiten je Aufruf

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 17:07:04 +02:00
co-authored by Claude Sonnet 5.5
parent 65a207488e
commit d4f235ae5e
8 changed files with 904 additions and 0 deletions
@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
use Brain\Monkey\Functions;
use Closure;
use LIT\MCP\Integrations\Divi\Design\AssignRewrite;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsAssignAbility;
use LIT\MCP\Integrations\Divi\Design\ModuleUsageQuery;
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
use LIT\MCP\Integrations\Divi\Design\PresetBook;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
use ReflectionProperty;
final class DiviPresetsAssignAbilityTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('current_user_can')->justReturn(true);
}
private function ability(PageRewriter $pages): DiviPresetsAssignAbility
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
return new DiviPresetsAssignAbility(
$writer,
$pages,
static fn (): PresetAssignment => new PresetAssignment(
static fn (): PresetBook => PresetBook::fromDivi(PresetBookTest::stored()),
static fn (string $module): array => $module === 'divi/text'
? ['content.decoration.bodyFont' => 'divi/font-body']
: [],
static fn (string $id): bool => preg_match('/^[0-9a-z]{10}$/', $id) === 1
)
);
}
/** A PageRewriter that must not be asked at all. */
private static function untouched(): PageRewriter&MockInterface
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->never();
$pages->expects('apply')->never();
return $pages;
}
private static function moduleOf(ModuleUsageQuery $query): string
{
return (new ReflectionProperty(ModuleUsageQuery::class, 'module'))->getValue($query);
}
private static function onlyWithOf(AssignRewrite $rewrite): ?string
{
return (new ReflectionProperty(AssignRewrite::class, 'onlyWithPresetId'))->getValue($rewrite);
}
public function testNeedsEditThemeOptionsAndTheVisualBuilder(): void
{
$ability = $this->ability(self::untouched());
Functions\when('current_user_can')->justReturn(false);
$input = ['module' => 'button', 'preset' => 'CTA primär'];
self::assertToolError($ability->permission($input), 'forbidden');
self::assertToolError($ability->execute($input), 'forbidden');
}
public function testUnknownPresetIsRefusedBeforeAnyPageIsRead(): void
{
$result = $this->ability(self::untouched())->execute(['module' => 'button', 'preset' => 'Fehlt']);
self::assertToolError($result, 'validation_failed');
self::assertArrayHasKey('preset', self::errorFields($result));
}
public function testOnlyWithAndOnlyWithoutPresetExcludeEachOther(): void
{
$result = $this->ability(self::untouched())->execute([
'module' => 'button',
'preset' => 'CTA primär',
'only_without_preset' => true,
'only_with_preset' => 'aaaaaaaaaa',
]);
self::assertToolError($result, 'validation_failed');
self::assertSame(
['only_with_preset' => 'nicht zusammen mit only_without_preset'],
self::errorFields($result)
);
}
public function testNullAndMissingAssignmentPointToDetach(): void
{
$ability = $this->ability(self::untouched());
$null = $ability->execute(['module' => 'button', 'preset' => null]);
self::assertToolError($null, 'validation_failed');
self::assertStringContainsString('lit/divi-design-detach', self::errorFields($null)['preset']);
$groupNull = $ability->execute(['module' => 'text', 'group_presets' => ['font-body' => null]]);
self::assertStringContainsString(
'lit/divi-design-detach',
self::errorFields($groupNull)['group_presets.font-body']
);
$missing = $ability->execute(['module' => 'button']);
self::assertSame('preset oder group_presets angeben', self::errorFields($missing)['preset']);
}
public function testBadModuleFiltersAndPostIdsAreCollected(): void
{
$ability = $this->ability(self::untouched());
$fields = self::errorFields($ability->execute([
'module' => 'Button!',
'preset' => 'CTA primär',
'post_ids' => [0],
'clear_overrides' => 'ja',
]));
self::assertSame(['module', 'post_ids', 'clear_overrides'], array_keys($fields));
$unknown = self::errorFields($ability->execute([
'module' => 'button',
'preset' => 'CTA primär',
'only_with_preset' => 'Fehlt',
]));
self::assertSame('unbekanntes Modul-Preset divi/button „Fehlt“', $unknown['only_with_preset']);
}
public function testDryRunListsThePagesAndWritesNothing(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')
->with(
Mockery::on(static fn (ModuleUsageQuery $query): bool => self::moduleOf($query) === 'divi/button'),
Mockery::on(static fn (Closure $rewrite): bool => $rewrite() instanceof AssignRewrite),
null
)
->andReturn(PageRewriteReportTest::preview(2));
$pages->expects('apply')->never();
$dry = self::assertToolError(
$this->ability($pages)->execute(['module' => 'button', 'preset' => ['CTA primär', 'cccccccccc']]),
'confirm_required'
);
self::assertSame([12, 13], array_column($dry['pages'], 'post_id'));
self::assertSame(2, $dry['pages_count']);
self::assertStringStartsWith('Zuweisung an divi/button – betroffen (Seiten: 2, Module: 2).', $dry['message']);
self::assertStringContainsString('confirm: true', $dry['message']);
}
public function testEmptyDryRunIsNothingToDo(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0));
$pages->expects('apply')->never();
$result = $this->ability($pages)->execute(['module' => 'button', 'preset' => 'CTA primär']);
self::assertTrue($result['ok']);
self::assertSame('Zuweisung an divi/button – keine Seite betroffen, nichts zu tun.', $result['summary']);
self::assertSame([[], 0], [$result['saved'], $result['remaining']]);
}
public function testConfirmWritesAndReportsWhatRemains(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->never();
$pages->expects('apply')
->with(
'lit/divi-presets-assign',
Mockery::on(static fn (ModuleUsageQuery $query): bool => self::moduleOf($query) === 'divi/button'),
Mockery::on(static fn (Closure $rewrite): bool => self::onlyWithOf($rewrite()) === 'aaaaaaaaaa'),
[12, 13],
'Zuweisung an divi/button'
)
->andReturn(PageRewriteReportTest::applied(2, 3));
$result = $this->ability($pages)->execute([
'module' => 'divi/button',
'preset' => ['bbbbbbbbbb', 'cccccccccc'],
'only_with_preset' => 'CTA primär',
'post_ids' => [12, 13],
'confirm' => true,
]);
self::assertTrue($result['ok']);
self::assertSame(3, $result['remaining']);
self::assertSame([900, 901], array_column($result['saved'], 'revision_id'));
self::assertStringContainsString('Noch 3 Seiten offen', $result['summary']);
self::assertStringContainsString('confirm: true', $result['summary']);
}
public function testSchemaAndMeta(): void
{
$ability = $this->ability(self::untouched());
$schema = $ability->inputSchema();
self::assertSame('lit/divi-presets-assign', $ability->name());
self::assertFalse($ability->isDirect());
self::assertSame(['readonly' => false, 'destructive' => false, 'idempotent' => true], $ability->annotations());
self::assertSame(['module'], $schema['required']);
self::assertFalse($schema['additionalProperties']);
self::assertSame(DiviPresetsAssignAbility::MAX_STACK, $schema['properties']['preset']['maxItems']);
self::assertArrayNotHasKey('design_rev', $schema['properties']);
self::assertSame(['saved', 'remaining', 'skipped'], array_slice($ability->outputSchema()['required'], -3));
self::assertStringContainsString('gewinnt das letzte Preset im Stapel', $ability->description());
}
}