test: split DiviWriteAbilitiesTest per tool (T5)
One test class per Divi write tool on a shared DiviWriteAbilityTestCase; the 30 tests move verbatim (per-test assertion counts identical). Adds the ledger T4 test for a TreeEditor::move() StructureRules refusal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
|
||||
final class DiviDeleteAbilityTest extends DiviWriteAbilityTestCase
|
||||
{
|
||||
/** The stored content equals the original minus exactly that block's bytes; revision_id is the restore point. */
|
||||
public function testDeleteAModuleRemovesExactlyItsBytes(): void
|
||||
{
|
||||
$buttonBytes = '<!-- wp:divi/button {"button":{"innerContent":{"desktop":{"value":{"text":"Termin buchen",'
|
||||
. '"linkUrl":"/kontakt","linkTarget":"off"}}}},"module":{"meta":{"adminLabel":{"desktop":{"value":"Hero –'
|
||||
. ' Button"}}}},"builderVersion":"5.2.1"} --><!-- /wp:divi/button -->';
|
||||
self::assertStringContainsString($buttonBytes, $this->original, 'precondition: exact bytes as expected');
|
||||
|
||||
$result = $this->delete()->execute([
|
||||
'post_id' => 7, 'tree_rev' => $this->treeRev(), 'ids' => ['s0.r0.c1.m2'], 'confirm' => true,
|
||||
]);
|
||||
|
||||
self::assertSame([['id' => 's0.r0.c1.m2', 'type' => 'divi/button', 'action' => 'deleted', 'label' => 'Hero – Button']], $result['changed']);
|
||||
self::assertSame(str_replace($buttonBytes, '', $this->original), $this->saved[0]);
|
||||
self::assertSame(811, $result['revision_id'], 'the restore-point revision');
|
||||
self::assertStringContainsString('Hero', (string) $result['outline'], 'the outline is embedded in the result');
|
||||
}
|
||||
|
||||
/** An opaque/global module is removed as a whole – it never gets descended into. */
|
||||
public function testDeleteRemovesAnOpaqueGlobalModuleAsAWhole(): void
|
||||
{
|
||||
$mixed = DiviFixtures::markup('synthetic-mixed.html');
|
||||
$this->post->post_content = $mixed;
|
||||
$globalRow = '<!-- wp:divi/row {"globalModule":"913","module":{"meta":{"adminLabel":{"desktop":{"value":'
|
||||
. '"Globale Zeile"}}}}} --><!-- wp:divi/column {"builderVersion":"5.2.1"} --><!-- wp:divi/text '
|
||||
. '{"content":{"innerContent":{"desktop":{"value":"lokal"}}}} /--><!-- /wp:divi/column --><!-- /wp:divi/row'
|
||||
. ' -->';
|
||||
self::assertStringContainsString($globalRow, $mixed, 'precondition: exact bytes as expected');
|
||||
|
||||
$result = $this->delete()->execute([
|
||||
'post_id' => 7, 'tree_rev' => Rev::of($mixed), 'ids' => ['s1.g1'], 'confirm' => true,
|
||||
]);
|
||||
|
||||
self::assertSame([['id' => 's1.g1', 'type' => 'divi/row', 'action' => 'deleted', 'label' => 'Globale Zeile']], $result['changed']);
|
||||
self::assertSame(str_replace($globalRow, '', $mixed), $this->saved[0]);
|
||||
}
|
||||
|
||||
public function testDeletingTheLastChildLeavesAValidTree(): void
|
||||
{
|
||||
$mixed = DiviFixtures::markup('synthetic-mixed.html');
|
||||
$this->post->post_content = $mixed;
|
||||
|
||||
$result = $this->delete()->execute([
|
||||
'post_id' => 7, 'tree_rev' => Rev::of($mixed), 'ids' => ['s0.c1.r0.c0.m0'], 'confirm' => true,
|
||||
]);
|
||||
|
||||
self::assertTrue($result['ok'], (string) json_encode($result));
|
||||
$index = NodeIndex::build((new Parser())->parse($this->saved[0]));
|
||||
$emptyColumn = $index->find('s0.c1.r0.c0');
|
||||
self::assertNotNull($emptyColumn, 'the now-empty column-inner is still a valid, findable node');
|
||||
self::assertSame([], $emptyColumn->children());
|
||||
}
|
||||
|
||||
public function testDeleteUnknownIdReturnsNotFound(): void
|
||||
{
|
||||
$result = $this->delete()->execute([
|
||||
'post_id' => 7, 'tree_rev' => $this->treeRev(), 'ids' => ['s9'], 'confirm' => true,
|
||||
]);
|
||||
|
||||
self::assertToolError($result, 'not_found');
|
||||
self::assertSame('not_found', self::clientError($result)['code']);
|
||||
self::assertSame([], $this->saved);
|
||||
}
|
||||
|
||||
/** Same annotations as the existing destructive tools (e.g. MediaDeleteAbility). */
|
||||
public function testDeleteAnnotationsMatchExistingDestructiveTools(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['readonly' => false, 'destructive' => true, 'idempotent' => false],
|
||||
$this->delete()->annotations()
|
||||
);
|
||||
}
|
||||
|
||||
/** Ruling S14 (Task 7): the deleted node's own confirm question, unaffected by other tests. */
|
||||
public function testDeleteAsksBeforeRemovingNestedNodesOnlyOnce(): void
|
||||
{
|
||||
$duplicated = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'id' => 's1']);
|
||||
self::assertSame('s2', $duplicated['changed'][0]['id']);
|
||||
|
||||
$delete = $this->delete();
|
||||
$ask = $delete->execute(['post_id' => 7, 'tree_rev' => Rev::of($this->saved[0]), 'ids' => ['s2', 's2.r0']]);
|
||||
self::assertToolError($ask, 'confirm_required');
|
||||
self::assertSame(
|
||||
'1 Divi-Knoten würden samt Inhalt entfernt: s2 section "Leistungen (Kopie)" (1 Kind-Knoten).',
|
||||
self::clientError($ask)['message']
|
||||
);
|
||||
|
||||
$deleted = $delete->execute(['post_id' => 7, 'tree_rev' => Rev::of($this->saved[0]), 'ids' => ['s2'], 'confirm' => true]);
|
||||
self::assertSame([['id' => 's2', 'type' => 'divi/section', 'action' => 'deleted', 'label' => 'Leistungen (Kopie)']], $deleted['changed']);
|
||||
self::assertSame($this->original, $this->saved[1], 'deleting the copy restores the exact bytes');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user