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
115 lines
5.8 KiB
PHP
115 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
||
|
||
use LIT\MCP\Abilities\Rev;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||
|
||
final class DiviDuplicateNodeAbilityTest extends DiviWriteAbilityTestCase
|
||
{
|
||
/** The copy sits directly after the original; its own child content is byte-identical, nothing else changes. */
|
||
public function testDuplicateInsertsTheCopyDirectlyAfterTheOriginal(): void
|
||
{
|
||
$sectionOpen = '<!-- wp:divi/section {"module":{"meta":{"adminLabel":{"desktop":{"value":"Leistungen"}}}},'
|
||
. '"builderVersion":"5.2.1"} -->';
|
||
$sectionClose = '<!-- /wp:divi/section -->';
|
||
$start = strpos($this->original, $sectionOpen) + strlen($sectionOpen);
|
||
$end = strpos($this->original, $sectionClose, $start);
|
||
$rowBytes = substr($this->original, $start, $end - $start);
|
||
$original = $sectionOpen . $rowBytes . $sectionClose;
|
||
self::assertStringContainsString($original, $this->original, 'precondition: exact bytes as expected');
|
||
|
||
$result = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => $this->treeRev(), 'id' => 's1']);
|
||
|
||
self::assertSame('s2', $result['changed'][0]['id']);
|
||
self::assertSame('Leistungen (Kopie)', $result['changed'][0]['label']);
|
||
self::assertStringContainsString('Leistungen', (string) $result['outline'], 'the outline is embedded in the result');
|
||
|
||
$copyOpen = str_replace('"Leistungen"', '"Leistungen (Kopie)"', $sectionOpen);
|
||
$copy = $copyOpen . $rowBytes . $sectionClose;
|
||
self::assertSame(
|
||
str_replace($original, $original . $copy, $this->original),
|
||
$this->saved[0],
|
||
'copy directly after the original, byte-identical child content, nothing else changes'
|
||
);
|
||
}
|
||
|
||
/** A global module copy stays a byte-identical reference (read-only nodes move/duplicate as a whole). */
|
||
public function testDuplicateGlobalModuleCopyIsAByteIdenticalReference(): 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->duplicate()->execute(['post_id' => 7, 'tree_rev' => Rev::of($mixed), 'id' => 's1.g1']);
|
||
|
||
self::assertSame('s1.g2', $result['changed'][0]['id']);
|
||
self::assertSame('divi/row', $result['changed'][0]['type']);
|
||
self::assertSame('Globale Zeile', $result['changed'][0]['label'], 'opaque copies keep their label unchanged');
|
||
self::assertSame(str_replace($globalRow, $globalRow . $globalRow, $mixed), $this->saved[0]);
|
||
}
|
||
|
||
/**
|
||
* Fix round 1 (Important-1): the copy's admin label must be the raw stored desktop value
|
||
* plus " (Kopie)" – never NodeSummary::label()'s display-shortened text (80-char limit).
|
||
*/
|
||
public function testDuplicateCopiesALongAdminLabelVerbatim(): void
|
||
{
|
||
$long = str_repeat('Ausführliche Beschriftung, ', 4) . 'Ende';
|
||
self::assertGreaterThan(80, strlen($long), 'precondition: longer than the NodeSummary::label() display limit');
|
||
$content = str_replace(
|
||
'"module":{"meta":{"adminLabel":{"desktop":{"value":"Hero – Button"}}}}',
|
||
'"module":{"meta":{"adminLabel":{"desktop":{"value":"' . $long . '"}}}}',
|
||
$this->original
|
||
);
|
||
self::assertNotSame($content, $this->original, 'precondition: replaced exactly once');
|
||
$this->post->post_content = $content;
|
||
|
||
$result = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => Rev::of($content), 'id' => 's0.r0.c1.m2']);
|
||
|
||
self::assertTrue($result['ok'], (string) json_encode($result));
|
||
$index = NodeIndex::build((new Parser())->parse($this->saved[0]));
|
||
$copy = $index->find('s0.r0.c1.m3');
|
||
self::assertNotNull($copy, 'the duplicate sits right after the original module');
|
||
self::assertSame(
|
||
$long . ' (Kopie)',
|
||
AttrMap::desktop($copy->attrs(), 'module.meta.adminLabel'),
|
||
'stored verbatim, not shortened/ellipsised for display'
|
||
);
|
||
}
|
||
|
||
public function testDuplicateCopiesALabelWithNewlinesAndMultipleSpacesVerbatim(): void
|
||
{
|
||
$messy = "Erste Zeile mit viel Leerraum\nZweite Zeile";
|
||
$encoded = substr((string) json_encode($messy, JSON_UNESCAPED_UNICODE), 1, -1);
|
||
$content = str_replace(
|
||
'"module":{"meta":{"adminLabel":{"desktop":{"value":"Hero – Button"}}}}',
|
||
'"module":{"meta":{"adminLabel":{"desktop":{"value":"' . $encoded . '"}}}}',
|
||
$this->original
|
||
);
|
||
self::assertNotSame($content, $this->original, 'precondition: replaced exactly once');
|
||
$this->post->post_content = $content;
|
||
|
||
$result = $this->duplicate()->execute(['post_id' => 7, 'tree_rev' => Rev::of($content), 'id' => 's0.r0.c1.m2']);
|
||
|
||
self::assertTrue($result['ok'], (string) json_encode($result));
|
||
$index = NodeIndex::build((new Parser())->parse($this->saved[0]));
|
||
$copy = $index->find('s0.r0.c1.m3');
|
||
self::assertNotNull($copy, 'the duplicate sits right after the original module');
|
||
self::assertSame(
|
||
$messy . ' (Kopie)',
|
||
AttrMap::desktop($copy->attrs(), 'module.meta.adminLabel'),
|
||
'whitespace/newlines kept verbatim, not collapsed'
|
||
);
|
||
}
|
||
}
|