Files

262 lines
12 KiB
PHP
Raw Permalink 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\Document;
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 LIT\MCP\Tests\Support\DiviFixtures;
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'], 'group_presets' => [], 'children' => 0,
'editable' => true, 'depth' => 3,
'truncated' => false, 'global_id' => null],
$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 testGroupPresetsAreShownWithNamesInRowAndText(): void
{
$content = DiviFixtures::markup('stage4/page--designsystem.html');
$outline = new Outline([
'vdcl6msnuo' => '[Fixture] Blurb Leistung',
'ja6o54jtlc' => '[Fixture] Fließtext',
'apzj7e17ok' => '[Fixture] Abstand Karte',
]);
$rows = $outline->rows(NodeIndex::build((new Parser())->parse($content)));
$byId = array_column($rows, null, 'id');
self::assertSame([], $byId['s0.r0.c0.m1']['group_presets']);
self::assertSame(['font-body: [Fixture] Fließtext'], $byId['s0.r0.c0.m2']['group_presets']);
self::assertSame(['spacing: [Fixture] Abstand Karte'], $byId['s0.r0.c0.m5']['group_presets']);
self::assertSame(['[Fixture] Blurb Leistung'], $byId['s0.r0.c0.m5']['presets']);
$lines = explode("\n", $outline->text($rows));
self::assertStringContainsString(
's0.r0.c0.m2 text "Text mit Gruppen-Preset" {Gruppen-Presets: font-body: [Fixture] Fließtext} – ',
$lines[5]
);
self::assertStringContainsString(
's0.r0.c0.m5 blurb "Blurb mit Modul- und Gruppen-Preset" {Preset: [Fixture] Blurb Leistung} '
. '{Gruppen-Presets: spacing: [Fixture] Abstand Karte} – Möbel nach Maß',
$lines[8]
);
self::assertStringNotContainsString('Gruppen-Presets', $lines[3]);
}
public function testUnknownGroupPresetFallsBackToTheIdAndStacksAreListed(): void
{
$block = Block::create('divi/blurb', ['groupPreset' => [
'module.decoration.spacing' => ['presetId' => ['a1', 'b2'], 'groupName' => 'divi/spacing'],
'title.decoration.font' => ['presetId' => ['c3'], 'groupName' => 'divi/font'],
]]);
$document = new Document();
$document->append($block);
$index = NodeIndex::build($document);
$outline = new Outline(['a1' => 'Abstand Karte']);
$rows = $outline->rows($index);
self::assertSame(['spacing: Abstand Karte, b2', 'font: c3'], $rows[0]['group_presets']);
self::assertStringContainsString(
'{Gruppen-Presets: spacing: Abstand Karte, b2; font: c3}',
$outline->text($rows)
);
}
public function testGlobalInstancesShowTheirLibraryIdAndRootType(): void
{
$content = DiviFixtures::markup('stage3/page--globale-instanzen.html');
$outline = new Outline();
$rows = $outline->rows(NodeIndex::build((new Parser())->parse($content)));
$byId = array_column($rows, null, 'id');
self::assertSame('1058', $byId['g0']['global_id']);
self::assertSame('section', $byId['g0']['type']);
self::assertFalse($byId['g0']['editable']);
self::assertSame('1059', $byId['s0.r0.c0.g0']['global_id']);
self::assertSame('text', $byId['s0.r0.c0.g0']['type']);
self::assertNull($byId['s0']['global_id']);
self::assertStringContainsString('g0 section "Global Abschnitt" [global 1058]', $outline->text($rows));
}
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" [global 913] (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);
}
/** Ruling P13: without library access the outline says "global", but not which entry. */
public function testGlobalIdsCanBeHidden(): void
{
$content = DiviFixtures::markup('stage3/page--globale-instanzen.html');
$outline = new Outline([], false);
$rows = $outline->rows(NodeIndex::build((new Parser())->parse($content)));
$byId = array_column($rows, null, 'id');
self::assertNull($byId['g0']['global_id']);
self::assertSame('section', $byId['g0']['type']);
self::assertStringNotContainsString('1058', $outline->text($rows));
self::assertStringNotContainsString('1059', $outline->text($rows));
self::assertStringNotContainsString('[global', $outline->text($rows));
self::assertStringContainsString('g0 section "Global Abschnitt" (nur lesbar)', $outline->text($rows));
self::assertStringNotContainsString('1058', (string) json_encode($rows));
self::assertStringNotContainsString('1059', (string) json_encode($rows));
$summary = $outline->summary(NodeIndex::build((new Parser())->parse($content)));
self::assertStringNotContainsString('1058', (string) $summary['text']);
$shown = new Outline();
self::assertStringContainsString('[global 1058]', $shown->text($shown->rows(NodeIndex::build((new Parser())->parse($content)))));
}
}