Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
273 lines
13 KiB
PHP
273 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
|
|
|
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
|
use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviNodeGetAbility;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviOutlineAbility;
|
|
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
|
use LIT\MCP\Tests\Support\DiviFixtures;
|
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use stdClass;
|
|
|
|
final class DiviReadAbilitiesTest extends TestCase
|
|
{
|
|
private function design(): DesignSystemReader
|
|
{
|
|
$reader = Mockery::mock(DesignSystemReader::class);
|
|
$reader->allows('read')->andReturn(DiviFixtures::designSystem());
|
|
|
|
return $reader;
|
|
}
|
|
|
|
/** The stage 4 preset inventory (tests/Fixtures/divi/stage4/presets.json) as the design system reads it. */
|
|
private function designWithGroupPresets(): DesignSystemReader
|
|
{
|
|
$stored = json_decode(DiviFixtures::markup('stage4/presets.json'), true)['stored'];
|
|
$reader = Mockery::mock(DesignSystemReader::class);
|
|
$reader->allows('read')->andReturn(DesignSystem::fromDivi([], [], $stored, []));
|
|
|
|
return $reader;
|
|
}
|
|
|
|
public function testOutlineShowsGroupPresetsWithNames(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('stage4/page--designsystem.html'));
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->designWithGroupPresets());
|
|
|
|
$result = $ability->execute(['post_id' => 7]);
|
|
$byId = array_column($result['nodes'], null, 'id');
|
|
|
|
self::assertSame([], $byId['s0.r0.c0.m1']['group_presets']);
|
|
self::assertSame(['[Fixture] Text Hinweis'], $byId['s0.r0.c0.m1']['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::assertStringContainsString(
|
|
'{Preset: [Fixture] Blurb Leistung} {Gruppen-Presets: spacing: [Fixture] Abstand Karte}',
|
|
$result['outline']
|
|
);
|
|
self::assertContains('group_presets', $ability->outputSchema()['properties']['nodes']['items']['required']);
|
|
}
|
|
|
|
public function testNodeGetListsGroupPresetsWithGroupAndNames(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('stage4/page--designsystem.html'));
|
|
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->designWithGroupPresets());
|
|
|
|
$blurb = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c0.m5']);
|
|
$plain = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c0.m3']);
|
|
|
|
self::assertSame(
|
|
[[
|
|
'group_id' => 'module.decoration.spacing',
|
|
'group' => 'spacing',
|
|
'presets' => [['id' => 'apzj7e17ok', 'name' => '[Fixture] Abstand Karte']],
|
|
]],
|
|
$blurb['group_presets']
|
|
);
|
|
self::assertSame([['id' => 'vdcl6msnuo', 'name' => '[Fixture] Blurb Leistung']], $blurb['presets']);
|
|
self::assertSame([], $plain['group_presets']);
|
|
self::assertArrayNotHasKey('groupPreset', $blurb['attrs']);
|
|
}
|
|
|
|
public function testOutlineListsNodesWithTreeRevAndPostRev(): void
|
|
{
|
|
$content = DiviFixtures::markup('synthetic-basic.html');
|
|
DiviFixtures::givenPost($content);
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
self::assertTrue($ability->permission(['post_id' => 7]));
|
|
$result = $ability->execute(['post_id' => 7]);
|
|
|
|
self::assertSame(substr(hash('sha256', $content), 0, 12), $result['tree_rev']);
|
|
self::assertSame(12, strlen($result['rev']));
|
|
self::assertNotSame($result['tree_rev'], $result['rev']);
|
|
self::assertCount(16, $result['nodes']);
|
|
self::assertStringStartsWith('s0 section "Hero"', $result['outline']);
|
|
self::assertFalse($result['truncated']);
|
|
self::assertSame([], $result['hints']);
|
|
self::assertTrue($ability->isDirect());
|
|
self::assertTrue($ability->annotations()['readonly']);
|
|
}
|
|
|
|
/** Ruling S18: the "limit" input caps children per parent; "truncated" reports the cut. */
|
|
public function testOutlineBreadthLimitTruncatesAndReportsIt(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
$result = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1', 'limit' => 2]);
|
|
|
|
self::assertSame(['s0.r0.c1', 's0.r0.c1.m0', 's0.r0.c1.m1'], array_column($result['nodes'], 'id'));
|
|
self::assertTrue($result['nodes'][0]['truncated']);
|
|
self::assertTrue($result['truncated']);
|
|
}
|
|
|
|
public function testOutlineOfASubtreeAndUnknownNode(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
$result = $ability->execute(['post_id' => 7, 'id' => 's1', 'depth' => 1]);
|
|
self::assertSame(['s1', 's1.r0'], array_column($result['nodes'], 'id'));
|
|
|
|
$missing = $ability->execute(['post_id' => 7, 'id' => 's7']);
|
|
self::assertToolError($missing, 'not_found');
|
|
self::assertStringContainsString('tree_rev', $missing->get_error_message());
|
|
}
|
|
|
|
public function testOutlineValidatesTheSubtreeId(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
self::assertSame(
|
|
'lit_validation_failed',
|
|
$ability->execute(['post_id' => 7, 'id' => 'hero'])->get_error_code()
|
|
);
|
|
}
|
|
|
|
public function testOutlineHintsForEmptyAndReadOnlyContent(): void
|
|
{
|
|
DiviFixtures::givenPost('');
|
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
self::assertStringContainsString('lit-divi-insert', $ability->execute(['post_id' => 7])['hints'][0]);
|
|
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-mixed.html'));
|
|
$hints = $ability->execute(['post_id' => 7])['hints'];
|
|
self::assertStringContainsString('nur lesbar', $hints[0]);
|
|
self::assertStringContainsString('nur als Ganzes', $hints[0]);
|
|
}
|
|
|
|
public function testNodeGetReturnsFullFormShortformAndPresets(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
|
|
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
$heading = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1.m0']);
|
|
|
|
self::assertSame('heading', $heading['type']);
|
|
self::assertSame('s0.r0.c1', $heading['parent']);
|
|
self::assertSame(12, strlen($heading['rev']));
|
|
self::assertSame(
|
|
['desktop' => ['value' => ['headingLevel' => 'h1', 'color' => '#1a1a1a']], 'phone' => ['value' => ['size' => '28px']]],
|
|
$heading['attrs']['title.decoration.font.font']
|
|
);
|
|
self::assertSame(['text' => 'Willkommen bei Müller & Söhne', 'level' => 'h1'], $heading['shortform']);
|
|
self::assertSame([['id' => 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', 'name' => '']], $heading['presets']);
|
|
self::assertSame('5.2.1', $heading['builder_version']);
|
|
|
|
$accordion = $ability->execute(['post_id' => 7, 'id' => 's1.r0.c1.m0']);
|
|
self::assertSame(['s1.r0.c1.m0.m0', 's1.r0.c1.m0.m1'], $accordion['children']);
|
|
self::assertInstanceOf(stdClass::class, $accordion['shortform']);
|
|
}
|
|
|
|
public function testNodeGetValidatesTheId(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('synthetic-basic.html'));
|
|
$ability = new DiviNodeGetAbility(new DiviPageLoader(DiviFixtures::schema()), $this->design());
|
|
|
|
self::assertToolError($ability->execute(['post_id' => 7, 'id' => 'hero']), 'validation_failed');
|
|
self::assertToolError($ability->execute(['post_id' => 7, 'id' => 's5.r0']), 'not_found');
|
|
}
|
|
|
|
public function testNodeGetNamesTheLibraryEntryOfAGlobalModule(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
|
|
$ability = new DiviNodeGetAbility(LibraryFixtures::loader(), $this->design());
|
|
|
|
$global = $ability->execute(['post_id' => 7, 'id' => 'g0']);
|
|
$plain = $ability->execute(['post_id' => 7, 'id' => 's0']);
|
|
|
|
self::assertSame('1058', $global['global']['id']);
|
|
self::assertSame('divi/section', $global['global']['block_name']);
|
|
self::assertStringContainsString('post_id 1058', $global['global']['hint']);
|
|
self::assertFalse($global['editable']);
|
|
self::assertNull($plain['global']);
|
|
}
|
|
|
|
/** Ruling P13: without the library right node-get and outline name no library ID. */
|
|
public function testWithoutLibraryAccessNoLibraryIdIsNamed(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
|
|
$loaders = [
|
|
'no divi_library right' => LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])),
|
|
'group disabled' => LibraryFixtures::loader(new LibraryAccess(
|
|
LibraryFixtures::store(),
|
|
LibraryFixtures::roles(),
|
|
null,
|
|
static fn (): bool => false
|
|
)),
|
|
'no library access at all' => new DiviPageLoader(DiviFixtures::schema()),
|
|
];
|
|
|
|
foreach ($loaders as $case => $loader) {
|
|
$nodeGet = new DiviNodeGetAbility($loader, $this->design());
|
|
$global = $nodeGet->execute(['post_id' => 7, 'id' => 'g0']);
|
|
$outline = (new DiviOutlineAbility($loader, $this->design()))->execute(['post_id' => 7]);
|
|
|
|
self::assertNull($global['global']['id'], $case);
|
|
self::assertSame('divi/section', $global['global']['block_name'], $case);
|
|
self::assertStringContainsString('Divi-Bibliothek', $global['global']['hint'], $case);
|
|
self::assertArrayNotHasKey('globalModule', $global['attrs'], $case);
|
|
self::assertFalse($global['editable'], $case);
|
|
self::assertStringNotContainsString('1058', (string) json_encode($global), $case);
|
|
self::assertStringNotContainsString('1058', (string) json_encode($outline), $case);
|
|
self::assertStringNotContainsString('1059', (string) json_encode($outline), $case);
|
|
self::assertStringNotContainsString('[global', (string) json_encode($outline), $case);
|
|
foreach (array_column($outline['nodes'], 'id') as $id) {
|
|
$node = $nodeGet->execute(['post_id' => 7, 'id' => $id]);
|
|
self::assertStringNotContainsString('1058', (string) json_encode($node), $case . ' ' . $id);
|
|
self::assertStringNotContainsString('1059', (string) json_encode($node), $case . ' ' . $id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Ruling P13: "nur lesen" (group read-only) does not hide library IDs. */
|
|
public function testAReadOnlyLibraryStillNamesLibraryIds(): void
|
|
{
|
|
DiviFixtures::givenPost(DiviFixtures::markup('stage3/page--globale-instanzen.html'));
|
|
$loader = LibraryFixtures::loader(LibraryFixtures::access([], false));
|
|
|
|
$global = (new DiviNodeGetAbility($loader, $this->design()))->execute(['post_id' => 7, 'id' => 'g0']);
|
|
$outline = (new DiviOutlineAbility($loader, $this->design()))->execute(['post_id' => 7]);
|
|
|
|
self::assertSame('1058', $global['global']['id']);
|
|
self::assertStringContainsString('[global 1058]', $outline['outline']);
|
|
}
|
|
|
|
public function testOutlineOfALibraryItem(): void
|
|
{
|
|
LibraryFixtures::given([LibraryFixtures::item(1059)]);
|
|
$ability = new DiviOutlineAbility(LibraryFixtures::loader(), $this->design());
|
|
|
|
$result = $ability->execute(['post_id' => 1059]);
|
|
|
|
self::assertSame(['m0'], array_column($result['nodes'], 'id'));
|
|
self::assertStringStartsWith('m0 text "Globales Modul Öffnungszeiten"', $result['outline']);
|
|
self::assertSame('https://kunde.test/wp-admin/post.php?post=1059&action=edit', $result['edit_url']);
|
|
}
|
|
|
|
public function testNodeGetOfALibraryItemAndNotFoundWithoutTheLibraryRight(): void
|
|
{
|
|
LibraryFixtures::given([LibraryFixtures::item(1057, ['post_status' => 'private', 'post_title' => 'Geheim'])]);
|
|
$ability = new DiviNodeGetAbility(LibraryFixtures::loader(), $this->design());
|
|
|
|
$result = $ability->execute(['post_id' => 1057, 'id' => 'm0']);
|
|
self::assertSame('m0', $result['id']);
|
|
|
|
$denied = new DiviNodeGetAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
|
$refusal = $denied->permission(['post_id' => 1057, 'id' => 'm0']);
|
|
self::assertToolError($refusal, 'not_found');
|
|
self::assertStringNotContainsString('Geheim', $refusal->get_error_message());
|
|
$outline = new DiviOutlineAbility(LibraryFixtures::loader(LibraryFixtures::access(['divi_library'])), $this->design());
|
|
self::assertToolError($outline->execute(['post_id' => 1057]), 'not_found');
|
|
}
|
|
}
|