Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/BlockTree/OutlineTest.php
T
janniclas.loosenandClaude Opus 5.5 d2cc82f31b refactor: small Divi tree helpers out of DiviWriter (M3)
Block::isPlaceholder replaces the divi/placeholder literals (ledger T4);
NodeIndex::parentBlock/sectionLabel replace the two parentBlock copies and
DiviWriter::sectionLabel; DiviChange::idOf and Outline::summary replace the
duplicated id resolution and outline text; DiviSchema holds the shared
input properties. Behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 18:24:29 +02:00

170 lines
7.2 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\BlockTree;
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use PHPUnit\Framework\TestCase;
use stdClass;
final class OutlineTest extends TestCase
{
public function testRowsAndTextOfAPage(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
$outline = new Outline(['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' => 'Hero-Titel']);
$rows = $outline->rows($index);
self::assertFalse($outline->wasTruncated());
self::assertSame(
['id' => 's0.r0.c1.m0', 'type' => 'heading', 'label' => '', 'excerpt' => 'Willkommen bei Müller & Söhne',
'info' => '', 'presets' => ['Hero-Titel'], 'children' => 0, 'editable' => true, 'depth' => 3,
'truncated' => false],
$rows[5]
);
self::assertSame('Spalten 8_24+16_24', $rows[1]['info']);
self::assertSame('Seit 1987 zuverlässig – jetzt anfragen.', $rows[6]['excerpt']);
self::assertSame(
's0 section "Hero"' . "\n"
. ' s0.r0 row "Hero – Zeile" [Spalten 8_24+16_24]' . "\n"
. ' s0.r0.c0 column' . "\n"
. ' s0.r0.c0.m0 image "Hero – Bild" – Unser Team vor dem Büro',
implode("\n", array_slice(explode("\n", $outline->text($rows)), 0, 4))
);
}
public function testSubtreeAndDepthLimit(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
$outline = new Outline();
$rows = $outline->rows($index, 's1.r0.c1', 1);
self::assertSame(['s1.r0.c1', 's1.r0.c1.m0'], array_column($rows, 'id'));
self::assertSame([0, 1], array_column($rows, 'depth'));
self::assertSame(2, $rows[1]['children']);
}
/** Ruling S18: outline breadth limit – caps children per parent, marks the parent truncated. */
public function testBreadthLimitCapsChildrenPerParentAndMarksTruncation(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
$outline = new Outline();
// s0.r0.c1 has three children (heading, text, button); cap them at two.
$rows = $outline->rows($index, 's0.r0.c1', null, 2);
self::assertSame(['s0.r0.c1', 's0.r0.c1.m0', 's0.r0.c1.m1'], array_column($rows, 'id'));
self::assertTrue($rows[0]['truncated'], 'the parent whose children were cut is marked');
self::assertFalse($rows[1]['truncated']);
self::assertTrue($outline->wasTruncated());
$full = (new Outline())->rows($index, 's0.r0.c1');
self::assertSame(4, count($full), 'the default limit (50) does not cut three children');
}
/** The page root has no row of its own, so a top-level cap can only show up in wasTruncated(). */
public function testBreadthLimitAtTheTopLevelStillReportsTruncation(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
$outline = new Outline();
$rows = $outline->rows($index, null, 0, 1);
self::assertSame(['s0'], array_column($rows, 'id'));
self::assertTrue($outline->wasTruncated());
}
public function testSummaryOfThePageOrOfSections(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-basic.html')));
$outline = new Outline();
self::assertSame(
['text' => $outline->text($outline->rows($index)), 'truncated' => false],
$outline->summary($index)
);
self::assertSame(
$outline->text($outline->rows($index, 's0')) . "\n" . $outline->text($outline->rows($index, 's1')),
$outline->summary($index, ['s0', 's1'])['text']
);
self::assertSame(['text' => null, 'truncated' => false], $outline->summary($index, []));
}
public function testReadOnlyNodesAreMarked(): void
{
$index = NodeIndex::build((new Parser())->parse($this->fixture('synthetic-mixed.html')));
$text = (new Outline())->text((new Outline())->rows($index));
self::assertStringContainsString('x0 core/paragraph (nur lesbar)', $text);
self::assertStringContainsString('s1.g1 row "Globale Zeile" (nur lesbar)', $text);
self::assertStringContainsString('s0 section [specialty]', $text);
self::assertStringContainsString('s0.c1.r0 row-inner [Spalten 1_2+1_2]', $text);
}
public function testSummaryHelpers(): void
{
self::assertSame('{post_title}', NodeSummary::textOf(
'$variable({"type":"content","value":{"name":"post_title","settings":{}}})$'
));
self::assertSame('Mehr', NodeSummary::textOf(['linkUrl' => '/x', 'text' => 'Mehr']));
self::assertSame('', NodeSummary::textOf(new stdClass()));
}
public function testLabelIsWhitespaceNormalizedAndBounded(): void
{
$block = self::blockWithAdminLabel("Hero \n Titel\twith space");
self::assertSame('Hero Titel with space', NodeSummary::label($block));
$long = str_repeat('a', 200);
$shortened = NodeSummary::label(self::blockWithAdminLabel($long));
self::assertLessThan(200, mb_strlen($shortened));
self::assertStringEndsWith('…', $shortened);
}
private static function blockWithAdminLabel(string $label): Block
{
return Block::create('divi/text', [
'module' => ['meta' => ['adminLabel' => ['desktop' => ['value' => $label]]]],
]);
}
public function testAttrMapFlattenGetSetRemove(): void
{
$attrs = [
'builderVersion' => '5.2.1',
'title' => ['innerContent' => ['desktop' => ['value' => 'Hallo']], 'decoration' => ['font' => [
'font' => ['desktop' => ['value' => ['headingLevel' => 'h2']], 'phone' => ['value' => ['size' => '20px']]],
]]],
'module' => ['decoration' => ['spacing' => ['desktop' => ['value' => new stdClass()]]]],
];
self::assertSame(
['module.decoration.spacing', 'title.decoration.font.font', 'title.innerContent'],
array_keys(AttrMap::flatten($attrs))
);
self::assertSame('Hallo', AttrMap::desktop($attrs, 'title.innerContent'));
self::assertTrue(AttrMap::isBreakpointMap(['desktop' => [], 'phone' => []]));
self::assertFalse(AttrMap::isBreakpointMap(['desktop' => [], 'font' => []]));
$changed = AttrMap::set($attrs, 'module.decoration.spacing.desktop.value.padding', '10px');
self::assertSame('10px', AttrMap::get($changed, 'module.decoration.spacing.desktop.value.padding'));
$removed = AttrMap::remove($attrs, 'title.decoration.font.font');
self::assertArrayNotHasKey('decoration', $removed['title']);
self::assertSame($attrs, AttrMap::remove($attrs, 'nope.path'));
}
private function fixture(string $name): string
{
return (string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/' . $name);
}
}