Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Schema/ModuleCatalogTest.php
T
janniclas.loosenandClaude Opus 5.5 02ae76d4f3 fix: stop guessing a kind for composite Divi attribute paths
Review fix round 1 on the lit/divi-catalog task:
- AttrPathIndex::collectKinds() no longer records a kind from a
  component.type === "group" node (a composite of several sub-kinds,
  e.g. button.innerContent = text + link + target), and kinds() now
  drops any attrName with more than one distinct candidate kind
  instead of last-write-wins (e.g. imageIcon.innerContent had onoff +
  icon + image sub-fields sharing one attrName). Both previously
  produced a wrong-but-present kind, which is more dangerous for 2b's
  sanitization routing than "unknown". Verified against the committed
  blurb/button/fullwidth-header fixture entries and live against the
  full 100-module catalog.
- AttrPathIndex::fromDefaults() now builds on AttrMap::flatten()
  instead of a parallel walker.
- ModuleCatalog::shortName() replaces the duplicated
  substr($name, strlen('divi/')) in DiviCatalogAbility.

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

171 lines
8.0 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\Schema;
use LIT\MCP\Integrations\Divi\Schema\AttrPathIndex;
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
use PHPUnit\Framework\TestCase;
final class ModuleCatalogTest extends TestCase
{
/** Real Divi 5.2.1 metadata (tests/Integration/divi-metadata-fixture.php). */
public static function sampleCatalog(): ModuleCatalog
{
$sample = json_decode(
(string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/metadata-sample.json'),
true
);
return new ModuleCatalog(
$sample['metadata'],
static fn (string $name): array => (array) ($sample['defaults'][$name] ?? [])
);
}
public function testKnowsModulesTitlesCategoriesAndChildren(): void
{
$catalog = self::sampleCatalog();
self::assertTrue($catalog->has('divi/button'));
self::assertFalse($catalog->has('button'));
self::assertSame('divi/button', ModuleCatalog::normalize(' Button '));
self::assertSame('divi/accordion-item', ModuleCatalog::normalize('divi/accordion-item'));
self::assertSame('structure', $catalog->category('divi/section'));
self::assertSame(['divi/accordion-item'], $catalog->childrenOf('divi/accordion'));
self::assertSame(['divi/column'], $catalog->childrenOf('divi/row'));
self::assertContains(
['name' => 'divi/heading', 'title' => 'Heading', 'category' => 'module', 'children' => []],
$catalog->summary()
);
}
public function testDerivesAttributePathsFromSettingsAndDefaults(): void
{
$catalog = self::sampleCatalog();
foreach (
[
'button.innerContent', 'button.decoration.button', 'button.decoration.background',
'module.advanced.alignment', 'module.decoration.spacing', 'module.meta.adminLabel',
'module.advanced.text.text',
] as $path
) {
self::assertTrue($catalog->isKnownPath('divi/button', $path), $path);
}
self::assertTrue($catalog->isKnownPath('divi/heading', 'title.decoration.font.font'));
self::assertFalse($catalog->isKnownPath('divi/heading', 'title.decoration.font'));
self::assertTrue($catalog->isKnownPath('divi/text', 'content.decoration.bodyFont.body.font'));
self::assertTrue($catalog->isKnownPath('divi/accordion-item', 'module.advanced.open'));
self::assertTrue($catalog->isKnownPath('divi/column', 'module.decoration.sizing'));
self::assertFalse($catalog->isKnownPath('divi/button', 'button.innerContent.text'));
self::assertSame('title.decoration.font.font', $catalog->suggestions('divi/heading', 'title.decoration.fnt.font')[0]);
}
public function testPathIndexFromHandwrittenMetadata(): void
{
$paths = AttrPathIndex::paths(
[
'title' => ['settings' => [
'innerContent' => ['groupType' => 'group-item', 'item' => ['attrName' => 'title.innerContent']],
'decoration' => ['font' => [], 'groupType' => 'x'],
]],
'module' => ['settings' => [
'meta' => ['meta' => []],
'advanced' => ['text' => ['item' => ['attrName' => 'module.advanced.alignment']]],
]],
],
['module' => ['advanced' => ['open' => ['desktop' => ['value' => 'off']]]], 'builderVersion' => '5']
);
self::assertSame(
[
'module.advanced.alignment', 'module.advanced.open', 'module.advanced.text.text',
'module.advanced.text.textShadow', 'module.meta.adminLabel', 'title.decoration.font.font',
'title.decoration.font.textShadow', 'title.innerContent',
],
$paths
);
}
public function testEveryShortformPathIsKnownForTheSampledModules(): void
{
$catalog = self::sampleCatalog();
foreach (['text', 'heading', 'button', 'image', 'blurb', 'code', 'accordion-item'] as $type) {
foreach (ShortformMap::fields($type) as $field => $spec) {
self::assertTrue(
$catalog->isKnownPath('divi/' . $type, $spec['path']),
sprintf('%s.%s → %s', $type, $field, $spec['path'])
);
}
}
}
public function testShortformTableIsConsistent(): void
{
$kinds = ['text', 'html', 'code', 'url', 'media', 'enum', 'onoff', 'level', 'number', 'ids', 'color'];
foreach (ShortformMap::types() as $type) {
$fields = ShortformMap::fields($type);
$primary = ShortformMap::primary($type);
self::assertTrue($primary === null || isset($fields[$primary]), $type . ' primary');
$child = ShortformMap::childType($type);
self::assertTrue($child === null || ShortformMap::has($child), $type . ' child');
foreach ($fields as $field => $spec) {
self::assertContains($spec['kind'], $kinds, $type . '.' . $field);
self::assertSame(1, preg_match('/^[a-zA-Z]+(\.[a-zA-Z]+)+$/', $spec['path']), $type . '.' . $field);
self::assertSame($spec['kind'] === 'media', isset($spec['keys']), $type . '.' . $field);
self::assertSame($spec['kind'] === 'enum', isset($spec['values']), $type . '.' . $field);
}
}
self::assertSame(
['text' => 'Termin', 'url' => '/kontakt'],
ShortformMap::values('button', ['button' => ['innerContent' => ['desktop' => ['value' => [
'text' => 'Termin', 'linkUrl' => '/kontakt',
]]]]])
);
}
public function testDerivesAttributePathKindsFromComponentMetadataWherePossible(): void
{
$catalog = self::sampleCatalog();
self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent'));
self::assertSame('text', $catalog->pathKind('divi/heading', 'title.innerContent'));
self::assertSame('code', $catalog->pathKind('divi/code', 'content.innerContent'));
self::assertSame('text', $catalog->pathKind('divi/button', 'module.meta.adminLabel'));
// Structural/style groups (fonts, backgrounds, …) carry no single scalar component in
// Divi's metadata; 2b falls back to key-name routing for them (spec §5.6, ruling R3).
self::assertSame('unknown', $catalog->pathKind('divi/button', 'button.decoration.background'));
self::assertSame('unknown', $catalog->pathKind('divi/button', 'no.such.path'));
// divi/image has no explicit attrName for its innerContent sub-fields at all.
self::assertSame('unknown', $catalog->pathKind('divi/image', 'image.innerContent'));
// Every kind in the module's kinds() map is a string, keyed by a known attribute path.
$paths = $catalog->paths('divi/heading');
foreach ($catalog->kinds('divi/heading') as $path => $kind) {
self::assertIsString($kind);
self::assertContains($path, $paths);
}
}
public function testLeavesCompositeAttributePathsUnknownRatherThanGuessing(): void
{
$catalog = self::sampleCatalog();
// button.innerContent's only attrName-annotated node is the "link" sub-group itself
// (component.type "group": link text + linkUrl + linkTarget) – not a single scalar kind.
self::assertSame('unknown', $catalog->pathKind('divi/button', 'button.innerContent'));
// imageIcon.innerContent carries three "field" components under the same attrName with
// different kinds (useIcon: onoff, icon: icon, src: image) – a genuine conflict.
self::assertSame('unknown', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent'));
// fullwidth-header's scrollDown.decoration.icon is the same pattern (onoff/icon/color/number).
self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'scrollDown.decoration.icon'));
}
}