Final fix wave for stage 2a, load-bearing for 2b's writers:
- Block::setAttrs()/Container::insertChild()/removeChild() refuse on an opaque
block (undecodable attrs or a global module) instead of silently corrupting it.
- Container::append() is now @internal (Parser only) and refuses on a void
block, so a child can no longer be silently dropped from the serialized output.
- Block::create() validates the block name against namespace/block-name.
- Json::decodeObject() returns null (-> opaque) for a top-level object whose
keys are a plain list ("0","1",...) rather than returning [] and letting the
Serializer re-encode it as a corrupting JSON array.
- AttrMap::set() rejects empty path segments and keeps an existing stdClass
value an object when rewrapping it (no silent object->list flip); AttrMap::get()
is updated in step so it can still read back through such an object.
- Block::copy() deep-clones stdClass attribute values instead of sharing them.
- Json::encode()'s existing JsonException-on-invalid-UTF-8 behaviour is documented.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
55 lines
1.8 KiB
PHP
55 lines
1.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\BlockTree;
|
|
|
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
|
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
|
|
use LogicException;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ContainerTest extends TestCase
|
|
{
|
|
public function testInsertChildRefusesOnAnOpaqueBlock(): void
|
|
{
|
|
$document = (new Parser())->parse('<!-- wp:divi/section {"globalModule":"x"} --><!-- /wp:divi/section -->');
|
|
$section = $document->children()[0];
|
|
self::assertTrue($section->isOpaque());
|
|
|
|
$this->expectException(LogicException::class);
|
|
$section->insertChild(0, Block::create('divi/text'));
|
|
}
|
|
|
|
public function testInsertChildRefusesOnABlockWithUndecodableAttrs(): void
|
|
{
|
|
$document = (new Parser())->parse('<!-- wp:divi/section {"a":} --><!-- /wp:divi/section -->');
|
|
$section = $document->children()[0];
|
|
self::assertTrue($section->isOpaque());
|
|
|
|
$this->expectException(LogicException::class);
|
|
$section->insertChild(0, Block::create('divi/text'));
|
|
}
|
|
|
|
public function testRemoveChildRefusesOnAnOpaqueBlock(): void
|
|
{
|
|
$document = (new Parser())->parse(
|
|
'<!-- wp:divi/section {"globalModule":"x"} --><!-- wp:divi/row --><!-- /wp:divi/row --><!-- /wp:divi/section -->'
|
|
);
|
|
$section = $document->children()[0];
|
|
$row = $section->children()[0];
|
|
self::assertTrue($section->isOpaque());
|
|
|
|
$this->expectException(LogicException::class);
|
|
$section->removeChild($row);
|
|
}
|
|
|
|
public function testAppendRefusesOnAVoidBlock(): void
|
|
{
|
|
$void = Block::parsed('divi/image', [], '<!-- wp:divi/image /-->', true, false);
|
|
|
|
$this->expectException(LogicException::class);
|
|
$void->append('text');
|
|
}
|
|
}
|