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:
co-authored by
Claude Sonnet 5.5
parent
65a207488e
commit
d4f235ae5e
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\PageRewriteReport;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PageRewriteReportTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A PageRewriter::preview() result: $pages listed pages (IDs from 12) with one module each.
|
||||
*
|
||||
* @param list<int> $blocked
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function preview(int $pages, array $blocked = []): array
|
||||
{
|
||||
$listed = [];
|
||||
for ($i = 0; $i < $pages; ++$i) {
|
||||
$listed[] = [
|
||||
'post_id' => 12 + $i,
|
||||
'title' => 'Seite ' . ($i + 1),
|
||||
'type' => 'page',
|
||||
'status' => 'publish',
|
||||
'nodes' => [['id' => 's0.r0.c0.m0', 'module' => 'divi/button', 'label' => 'Button']],
|
||||
'warnings' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'pages' => $listed,
|
||||
'pages_count' => $pages,
|
||||
'nodes_count' => $pages,
|
||||
'skipped' => [],
|
||||
'blocked_ids' => $blocked,
|
||||
'hidden_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A PageRewriter::apply() result: $saved pages (IDs from 12) with two modules each.
|
||||
*
|
||||
* @param list<array{post_id: int, reason: string}> $skipped
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function applied(int $saved, int $remaining, array $skipped = []): array
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 0; $i < $saved; ++$i) {
|
||||
$rows[] = ['post_id' => 12 + $i, 'revision_id' => 900 + $i, 'nodes' => 2];
|
||||
}
|
||||
|
||||
return ['saved' => $rows, 'remaining' => $remaining, 'warnings' => [], 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
public function testEmptyMeansNothingToWriteNothingBlockedNothingHidden(): void
|
||||
{
|
||||
self::assertTrue(PageRewriteReport::isEmpty(self::preview(0)));
|
||||
self::assertFalse(PageRewriteReport::isEmpty(self::preview(1)));
|
||||
self::assertFalse(PageRewriteReport::isEmpty(self::preview(0, [7])));
|
||||
self::assertFalse(PageRewriteReport::isEmpty(['hidden_count' => 1] + self::preview(0)));
|
||||
}
|
||||
|
||||
public function testDryRunMessageNamesCountsTheLimitAndBlockedPages(): void
|
||||
{
|
||||
$message = PageRewriteReport::dryRunMessage(self::preview(2), 'Zuweisung an divi/button');
|
||||
|
||||
self::assertStringStartsWith('Zuweisung an divi/button – betroffen (Seiten: 2, Module: 2).', $message);
|
||||
self::assertStringContainsString('Nichts wurde gespeichert (Probelauf).', $message);
|
||||
self::assertStringContainsString('confirm: true', $message);
|
||||
self::assertStringContainsString('höchstens 50 Seiten', $message);
|
||||
self::assertStringNotContainsString('blocked_ids', $message);
|
||||
self::assertStringContainsString('blocked_ids', PageRewriteReport::dryRunMessage(self::preview(1, [7]), 'X'));
|
||||
}
|
||||
|
||||
public function testSavedSummaryAsksForAnotherCallWhilePagesRemain(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'X – gespeichert (Seiten: 2, Module: 4). Noch 3 Seiten offen: mit denselben Angaben erneut mit '
|
||||
. 'confirm: true aufrufen.',
|
||||
PageRewriteReport::savedSummary(self::applied(2, 3), 'X')
|
||||
);
|
||||
self::assertSame(
|
||||
'X – gespeichert (Seiten: 1, Module: 2). Alle Fundstellen sind erledigt.',
|
||||
PageRewriteReport::savedSummary(self::applied(1, 0), 'X')
|
||||
);
|
||||
self::assertSame(
|
||||
'X – gespeichert (Seiten: 0, Module: 0). Übersprungen: 1 (siehe skipped).',
|
||||
PageRewriteReport::savedSummary(self::applied(0, 0, [['post_id' => 5, 'reason' => 'global']]), 'X')
|
||||
);
|
||||
}
|
||||
|
||||
public function testNothingToDoIsAToolResultWithTheSkippedPages(): void
|
||||
{
|
||||
$preview = ['skipped' => [['post_id' => 5, 'reason' => 'global']]] + self::preview(0);
|
||||
|
||||
$result = PageRewriteReport::nothingToDo('Nichts zu tun.', $preview, ['Hinweis']);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('Nichts zu tun.', $result['summary']);
|
||||
self::assertSame(['Hinweis'], $result['warnings']);
|
||||
self::assertSame([[], 0], [$result['saved'], $result['remaining']]);
|
||||
self::assertSame(5, $result['skipped'][0]['post_id']);
|
||||
}
|
||||
|
||||
public function testOutputSchemaRequiresTheRewriteKeys(): void
|
||||
{
|
||||
$schema = PageRewriteReport::outputSchema(['diff' => ['type' => 'array']]);
|
||||
|
||||
self::assertSame(['saved', 'remaining', 'skipped'], array_slice($schema['required'], -3));
|
||||
self::assertSame('integer', $schema['properties']['remaining']['type']);
|
||||
self::assertSame(['type' => 'array'], $schema['properties']['diff']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
|
||||
use LIT\MCP\Integrations\Divi\Design\PostIdsInput;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PostIdsInputTest extends TestCase
|
||||
{
|
||||
public function testParsesAListOfPositiveIdsWithoutDuplicates(): void
|
||||
{
|
||||
self::assertNull(PostIdsInput::parse(null));
|
||||
self::assertSame([12, 13], PostIdsInput::parse([12, 13, 12]));
|
||||
self::assertSame(PageRewriter::PAGE_LIMIT, PostIdsInput::MAX);
|
||||
}
|
||||
|
||||
/** @dataProvider invalidInput */
|
||||
public function testRefusesEverythingElse(mixed $value): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Liste von 1 bis 50 Beitrags-IDs (positive ganze Zahlen) erwartet');
|
||||
|
||||
PostIdsInput::parse($value);
|
||||
}
|
||||
|
||||
/** @return array<string, array{mixed}> */
|
||||
public static function invalidInput(): array
|
||||
{
|
||||
return [
|
||||
'leer' => [[]],
|
||||
'null als ID' => [[0]],
|
||||
'negativ' => [[-3]],
|
||||
'Text' => [['12']],
|
||||
'Objekt' => [['a' => 1]],
|
||||
'zu viele' => [range(1, 51)],
|
||||
'keine Liste' => [12],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSchemaMatchesTheParser(): void
|
||||
{
|
||||
$schema = PostIdsInput::schema();
|
||||
|
||||
self::assertSame('array', $schema['type']);
|
||||
self::assertSame(1, $schema['minItems']);
|
||||
self::assertSame(50, $schema['maxItems']);
|
||||
self::assertSame(['type' => 'integer', 'minimum' => 1], $schema['items']);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ final class DiviIntegrationTest extends TestCase
|
||||
'lit/divi-presets-delete',
|
||||
'lit/divi-design-snapshots',
|
||||
'lit/divi-design-restore',
|
||||
'lit/divi-presets-assign',
|
||||
];
|
||||
|
||||
public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void
|
||||
|
||||
Reference in New Issue
Block a user