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

326 lines
14 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 Closure;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DetachRewrite;
use LIT\MCP\Integrations\Divi\Design\DiviDesignDetachAbility;
use LIT\MCP\Integrations\Divi\Design\PageRewriter;
use LIT\MCP\Integrations\Divi\Design\PresetBook;
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\Unit\TestCase;
use Mockery;
use Mockery\MockInterface;
use ReflectionProperty;
final class DiviDesignDetachAbilityTest extends TestCase
{
private const LESETEXT = ['group' => 'font-body', 'preset' => 'Lesetext'];
protected function setUp(): void
{
parent::setUp();
Functions\when('current_user_can')->justReturn(true);
// DesignLock (final review I-5): the lock is always free here.
Functions\when('add_option')->justReturn(true);
Functions\when('delete_option')->justReturn(true);
}
private function ability(
PageRewriter $pages,
?DesignSystemWriter $writer = null,
?SnapshotStore $snapshots = null,
?FakeChangeLog $log = null
): DiviDesignDetachAbility {
$usages = Mockery::mock(GlobalUsages::class);
$usages->allows('reportFor')->andReturn(new UsageReport(0, [], 0, 0, [], 0, false));
return new DiviDesignDetachAbility(
$writer ?? self::readOnlyWriter(),
new DesignChange($snapshots ?? Mockery::mock(SnapshotStore::class), $log ?? FakeChangeLog::forbidden()),
$usages,
$pages,
static fn (): bool => true
);
}
/** Reads PresetBookTest::stored(), never saves. */
private static function readOnlyWriter(): DesignSystemWriter&MockInterface
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')->never();
return $writer;
}
private static function untouched(): PageRewriter&MockInterface
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->never();
$pages->expects('apply')->never();
return $pages;
}
/** Matches the explicit-only query and a DetachRewrite strategy of a preview()/apply() call. */
private static function explicitQuery(): Closure
{
return static function (PresetUsageQuery $query): bool {
$targets = (new ReflectionProperty(PresetUsageQuery::class, 'targets'))->getValue($query);
return array_column($targets, 'default') === [false];
};
}
private static function detaching(): Closure
{
return static fn (Closure $rewrite): bool => $rewrite() instanceof DetachRewrite;
}
public function testNeedsEditThemeOptionsAndTheVisualBuilder(): void
{
$ability = $this->ability(self::untouched());
Functions\when('current_user_can')->justReturn(false);
self::assertToolError($ability->permission(['preset' => self::LESETEXT]), 'forbidden');
self::assertToolError($ability->execute(['preset' => self::LESETEXT]), 'forbidden');
}
public function testVariablesFollowWithStage4c(): void
{
$error = self::assertToolError(
$this->ability(self::untouched())->execute(['variable' => 'Eckenradius']),
'unsupported'
);
self::assertStringContainsString('Variablen folgen mit Etappe 4c.', $error['hint']);
}
public function testUnknownAndAmbiguousPresets(): void
{
$ability = $this->ability(self::untouched());
$unknown = $ability->execute(['preset' => ['module' => 'button', 'preset' => 'Fehlt']]);
self::assertToolError($unknown, 'not_found');
$ambiguous = $ability->execute(['preset' => ['module' => 'button', 'preset' => 'Doppelt']]);
self::assertToolError($ambiguous, 'validation_failed');
self::assertStringContainsString('bbbbbbbbbb, cccccccccc', self::errorFields($ambiguous)['preset.preset']);
$form = $ability->execute(['preset' => ['preset' => 'Lesetext']]);
self::assertArrayHasKey('preset', self::errorFields($form));
}
public function testDryRunListsPagesAndTheDiffOfTheNestingPresets(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')
->with(Mockery::on(self::explicitQuery()), Mockery::on(self::detaching()), null)
->andReturn(PageRewriteReportTest::preview(1));
$pages->expects('apply')->never();
$dry = self::assertToolError($this->ability($pages)->execute(['preset' => self::LESETEXT]), 'confirm_required');
self::assertSame(12, $dry['pages'][0]['post_id']);
self::assertSame([['dddddddddd', 'changed']], array_map(
static fn (array $row): array => [$row['id'], $row['action']],
$dry['diff']
));
self::assertSame(PresetBook::fromDivi(PresetBookTest::stored())->rev(), $dry['design_rev']);
self::assertStringContainsString('design_rev', $dry['message']);
self::assertSame([], $dry['warnings']);
}
public function testConfirmWithAStaleDesignRevWritesNeitherPresetsNorPages(): void
{
$error = self::assertToolError(
$this->ability(self::untouched())->execute([
'preset' => self::LESETEXT,
'confirm' => true,
'design_rev' => 'veraltet',
]),
'rev_mismatch'
);
self::assertStringContainsString('nichts wurde gespeichert', $error['message']);
}
public function testConfirmRemovesTheNestedReferencesFirstThenThePages(): void
{
$calls = [];
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
->with(
Mockery::on(static fn (array $rest): bool => $rest['module'][1]['items'][0]['groupPresets'] === []),
false
)
->andReturnUsing(static function () use (&$calls): array {
$calls[] = 'presets';
return PresetBook::fromDivi(PresetBookTest::stored())->withoutNestedRef('gggggggggg')->data();
});
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->expects('save')->andReturn('presets-4');
$snapshots->expects('attachLog');
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(1));
$pages->expects('apply')
->with(
'lit/divi-design-detach',
Mockery::on(self::explicitQuery()),
Mockery::on(self::detaching()),
null,
'Entkoppeln von Gruppen-Preset divi/font-body „Lesetext“'
)
->andReturnUsing(static function () use (&$calls): array {
$calls[] = 'pages';
return PageRewriteReportTest::applied(1, 0);
});
$ability = $this->ability($pages, $writer, $snapshots, new FakeChangeLog(7));
$dry = self::assertToolError($ability->execute(['preset' => self::LESETEXT]), 'confirm_required');
$result = $ability->execute([
'preset' => self::LESETEXT,
'confirm' => true,
'design_rev' => $dry['design_rev'],
]);
self::assertSame(['presets', 'pages'], $calls);
self::assertSame('presets-4', $result['snapshot_id']);
self::assertSame(7, $result['log_id']);
self::assertSame('dddddddddd', $result['diff'][0]['id']);
self::assertSame(12, $result['saved'][0]['post_id']);
self::assertStringStartsWith('Presets: 1 geändert.', $result['summary']);
self::assertStringEndsWith('Jetzt kann lit/divi-presets-delete das Preset löschen.', $result['summary']);
}
public function testAPageErrorAfterThePresetStepSaysThePresetsAreChanged(): void
{
$writer = Mockery::mock(DesignSystemWriter::class);
$writer->allows('presets')->andReturn(PresetBookTest::stored());
$writer->allows('canUseVisualBuilder')->andReturn(true);
$writer->expects('savePresets')
->andReturn(PresetBook::fromDivi(PresetBookTest::stored())->withoutNestedRef('gggggggggg')->data());
$snapshots = Mockery::mock(SnapshotStore::class);
$snapshots->expects('save')->andReturn('presets-4');
$snapshots->expects('attachLog');
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('apply')->andReturn(Errors::partialWrite('Seite 13 nicht gespeichert.', 13));
$rev = PresetBook::fromDivi(PresetBookTest::stored())->rev();
$error = self::assertToolError(
$this->ability($pages, $writer, $snapshots, new FakeChangeLog(7))->execute([
'preset' => self::LESETEXT,
'confirm' => true,
'design_rev' => $rev,
]),
'partial_write'
);
self::assertStringContainsString('bereits entfernt (Snapshot presets-4)', $error['message']);
self::assertSame('presets-4', $error['snapshot_id']);
self::assertSame(13, $error['post_id']);
}
public function testPostIdsTouchOnlyThePages(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')
->with(Mockery::on(self::explicitQuery()), Mockery::on(self::detaching()), [12])
->andReturn(PageRewriteReportTest::preview(1));
$pages->expects('apply')
->with('lit/divi-design-detach', Mockery::any(), Mockery::any(), [12], Mockery::type('string'))
->andReturn(PageRewriteReportTest::applied(1, 0));
$ability = $this->ability($pages);
$input = ['preset' => self::LESETEXT, 'post_ids' => [12]];
$dry = self::assertToolError($ability->execute($input), 'confirm_required');
self::assertArrayNotHasKey('diff', $dry);
$result = $ability->execute($input + ['confirm' => true]);
self::assertSame([], $result['diff']);
self::assertNull($result['design_rev']);
self::assertNull($result['snapshot_id']);
self::assertStringNotContainsString('lit/divi-presets-delete', $result['summary']);
}
public function testADefaultPresetLosesOnlyItsExplicitReferences(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')
->with(Mockery::on(self::explicitQuery()), Mockery::on(self::detaching()), null)
->andReturn(PageRewriteReportTest::preview(1));
$dry = self::assertToolError(
$this->ability($pages)->execute(['preset' => ['module' => 'text', 'preset' => 'Fließtext']]),
'confirm_required'
);
self::assertSame(
['„Fließtext“ ist das Standard-Preset von divi/text: Divi wendet es weiter auf Module ohne eigenes '
. 'Preset an; entfernt werden nur die ausdrücklichen Verweise.'],
$dry['warnings']
);
self::assertArrayNotHasKey('diff', $dry, 'module presets are never nested');
}
public function testNothingLeftIsNoConfirmAndPointsToDelete(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('preview')->andReturn(PageRewriteReportTest::preview(0));
$result = $this->ability($pages)->execute(['preset' => ['module' => 'button', 'preset' => 'CTA primär']]);
self::assertTrue($result['ok']);
self::assertSame(
'Keine Verweise auf Modul-Preset divi/button „CTA primär“ gefunden – nichts zu tun. Jetzt kann '
. 'lit/divi-presets-delete das Preset löschen.',
$result['summary']
);
}
public function testRemainingPagesAskForAnotherCallWithoutTheDeleteHint(): void
{
$pages = Mockery::mock(PageRewriter::class);
$pages->expects('apply')->andReturn(PageRewriteReportTest::applied(2, 2));
$result = $this->ability($pages)->execute([
'preset' => ['module' => 'button', 'preset' => 'CTA primär'],
'confirm' => true,
]);
self::assertSame(2, $result['remaining']);
self::assertStringContainsString('Noch 2 Seiten offen', $result['summary']);
self::assertStringNotContainsString('lit/divi-presets-delete', $result['summary']);
}
public function testSchemaAndMeta(): void
{
$ability = $this->ability(self::untouched());
$schema = $ability->inputSchema();
self::assertSame('lit/divi-design-detach', $ability->name());
self::assertFalse($ability->isDirect());
self::assertSame(['readonly' => false, 'destructive' => false, 'idempotent' => true], $ability->annotations());
self::assertFalse($schema['additionalProperties']);
self::assertSame(['preset'], $schema['properties']['preset']['required']);
self::assertArrayHasKey('design_rev', $schema['properties']);
self::assertArrayHasKey('variable', $schema['properties']);
self::assertSame(['saved', 'remaining', 'skipped'], array_slice($ability->outputSchema()['required'], -3));
self::assertStringContainsString('lit/divi-presets-delete', $ability->description());
}
}