Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Compose/TreeEditorTest.php
T
janniclas.loosenandClaude Opus 5.5 3d96a41366 test: shared Divi fixtures in tests/Support (T1)
- DiviFixtures: markup(), givenPost(), schema(), designSystem(), catalog(),
  dynamicContentRegistry(), maliciousRefs() - moved verbatim out of
  DiviPageLoaderTest, DesignSystemTest, ModuleCatalogTest, DynamicContentMapTest
  and VariableRefTest (its malicious() provider now returns maliciousRefs()).
- ComposeFixture moves to tests/Support.
- DiviWriterFixture: modules(), designReader(), composition(), a recording saver(),
  log() and writer(&$saved, &$failures, unfiltered, loader) for the five tests that
  wired the Divi write stack by hand.
No test cases or assertions changed (1397 / 5834).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 17:47:11 +02:00

184 lines
8.6 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\Compose;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\Document;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Compose\TreeEditor;
use LIT\MCP\Tests\Support\ComposeFixture;
use PHPUnit\Framework\TestCase;
final class TreeEditorTest extends TestCase
{
private function document(string $name = 'synthetic-basic.html'): Document
{
return (new Parser())->parse((string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/' . $name));
}
public function testResolvesPositions(): void
{
$document = $this->document();
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$end = $editor->position($index, []);
self::assertSame($index->root(), $end['container']);
self::assertNull($end['parent']);
$inColumn = $editor->position($index, ['parent' => 's0.r0.c1', 'index' => 1]);
self::assertSame($index->find('s0.r0.c1'), $inColumn['parent']);
self::assertSame(1, $inColumn['index']);
$after = $editor->position($index, ['after' => 's0.r0.c1.m0']);
self::assertSame($index->find('s0.r0.c1'), $after['container']);
self::assertSame(1, $after['index']);
$beforeSection = $editor->position($index, ['before' => 's1']);
self::assertNull($beforeSection['parent']);
self::assertSame(1, $beforeSection['index']);
self::assertSame('nur eines von parent, before oder after angeben', $editor->position($index, ['parent' => 's0', 'after' => 's1']));
self::assertSame('Knoten "s9" gibt es nicht', $editor->position($index, ['before' => 's9']));
}
public function testInsertChecksTheStructureRules(): void
{
$document = $this->document();
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$button = Block::create('divi/button');
self::assertSame(
'auf oberster Ebene sind nur Abschnitte (section) erlaubt',
$editor->insert($editor->position($index, []), [$button])
);
self::assertStringContainsString('Zeilen', (string) $editor->insert($editor->position($index, ['parent' => 's0']), [$button]));
self::assertStringContainsString('Spalten entstehen mit der Zeile', (string) $editor->insert($editor->position($index, ['parent' => 's0.r0']), [$button]));
self::assertStringContainsString('accordion-item', (string) $editor->insert($editor->position($index, ['parent' => 's1.r0.c1.m0']), [$button]));
self::assertNull($editor->insert($editor->position($index, ['after' => 's0.r0.c1.m0']), [$button]));
self::assertSame('divi/button', NodeIndex::build($document)->find('s0.r0.c1.m1')?->name());
}
/**
* Ruling S9: inserting at the document root of an empty page (no divi/placeholder yet)
* wraps it in one first, the way DiviContentComposer (Task 9) writes new content.
*/
public function testInsertIntoAnEmptyPageCreatesAPlaceholder(): void
{
$document = new Document();
$index = NodeIndex::build($document);
self::assertSame($document, $index->root());
$editor = new TreeEditor(ComposeFixture::rules());
$section = Block::create('divi/section');
self::assertNull($editor->insert($editor->position($index, []), [$section]));
self::assertSame(
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->',
Serializer::document($document)
);
$rebuilt = NodeIndex::build($document);
self::assertSame($section, $rebuilt->find('s0'));
self::assertNull($rebuilt->parentId($section), 'the placeholder is transparent, like every other top-level node');
// A second insert on the now non-empty page must not wrap it again.
$second = Block::create('divi/section');
self::assertNull($editor->insert($editor->position($rebuilt, []), [$second]));
self::assertSame(1, substr_count(Serializer::document($document), '<!-- wp:divi/placeholder -->'));
}
public function testMoveWithinAndAcrossParents(): void
{
$document = $this->document();
$original = Serializer::document($document);
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$button = $index->find('s0.r0.c1.m2');
self::assertNull($editor->move($index, $button, $editor->position($index, ['before' => 's0.r0.c1.m0'])));
$index = NodeIndex::build($document);
self::assertSame($button, $index->find('s0.r0.c1.m0'));
self::assertNull($editor->move($index, $button, $editor->position($index, ['parent' => 's1.r0.c0'])));
$index = NodeIndex::build($document);
self::assertSame($button, $index->find('s1.r0.c0.m1'));
self::assertStringContainsString('Termin buchen', Serializer::document($document));
// Ruling S14: move it back (exact inverse of the two moves above) and assert the
// serialized page is byte-identical to the original – not merely the same length.
self::assertNull($editor->move($index, $button, $editor->position($index, ['before' => 's0.r0.c1.m0'])));
$index = NodeIndex::build($document);
self::assertNull($editor->move($index, $button, $editor->position($index, ['after' => 's0.r0.c1.m2'])));
self::assertSame($original, Serializer::document($document), 'moving a node back restores every byte');
$section = $index->find('s0');
self::assertSame(
'ein Knoten kann nicht in sich selbst verschoben werden',
$editor->move($index, $section, $editor->position($index, ['parent' => 's0.r0.c0']))
);
self::assertNull($editor->move($index, $section, $editor->position($index, [])));
self::assertSame($section, NodeIndex::build($document)->find('s1'));
}
public function testRemoveAndDuplicate(): void
{
$document = $this->document();
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$copy = $editor->duplicate($index, $index->find('s1.r0.c1.m0'));
$index = NodeIndex::build($document);
self::assertSame($copy, $index->find('s1.r0.c1.m1'));
self::assertSame(Serializer::block($index->find('s1.r0.c1.m0')), Serializer::block($copy));
$editor->remove($index, $index->find('s0'));
self::assertSame('divi/section', NodeIndex::build($document)->find('s0')?->name());
self::assertNull(NodeIndex::build($document)->find('s1'));
}
/**
* Duplicate never mutates an opaque block's internals; a copy of a global module stays a
* byte-identical reference, and the original is untouched (spec §5: byte-identical roundtrip).
*/
public function testDuplicateNeverMutatesAnOpaqueGlobalModule(): void
{
$document = $this->document('synthetic-mixed.html');
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$globalRow = $index->find('s1.g1');
self::assertNotNull($globalRow);
self::assertTrue($globalRow->isOpaque());
$originalBytes = Serializer::block($globalRow);
$copy = $editor->duplicate($index, $globalRow);
self::assertNotSame($globalRow, $copy);
self::assertTrue($copy->isOpaque());
self::assertSame($originalBytes, Serializer::block($copy), 'the copy is byte-identical to the original');
self::assertSame($originalBytes, Serializer::block($globalRow), 'the original is left untouched');
$index = NodeIndex::build($document);
self::assertSame($globalRow, $index->find('s1.g1'));
self::assertSame($copy, $index->find('s1.g2'));
}
public function testReadOnlyTargetsAndSpecialtySections(): void
{
$document = $this->document('synthetic-mixed.html');
$index = NodeIndex::build($document);
$editor = new TreeEditor(ComposeFixture::rules());
$text = Block::create('divi/text');
self::assertStringContainsString('nur lesbar', (string) $editor->insert($editor->position($index, ['parent' => 's1.g1']), [$text]));
self::assertStringContainsString('Spezialabschnitte', (string) $editor->insert($editor->position($index, ['parent' => 's0']), [Block::create('divi/row')]));
self::assertNull($editor->insert($editor->position($index, ['parent' => 's0.c0']), [$text]));
}
}