(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), and its "text" sub-field // carries no attrName of its own – Divi's metadata gives no reliable subName => kind // map here at all, so the whole path stays unknown (never guessed). self::assertSame('unknown', $catalog->pathKind('divi/button', 'button.innerContent')); self::assertArrayNotHasKey('button.innerContent', $catalog->kinds('divi/button')); } /** * Composite paths (Divi groups several named sub-fields, "subName", under one attribute * path) resolve to a subName => kind map wherever the metadata reliably names each * sub-field's own component (spec §5.6, ruling F1 #1) – asking without a subName, or for * a subName the metadata never names (e.g. an image's auto-synced "alt"), stays unknown. */ public function testDerivesPerSubNameKindsForCompositePaths(): void { $catalog = self::sampleCatalog(); // imageIcon.innerContent (divi/blurb): useIcon (toggle), icon (icon-picker), src // (upload, image) each carry attrName + subName + their own field component. self::assertSame( ['useIcon' => 'onoff', 'icon' => 'icon', 'src' => 'image'], $catalog->kinds('divi/blurb')['imageIcon.innerContent'] ); self::assertSame('onoff', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'useIcon')); self::assertSame('icon', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'icon')); self::assertSame('image', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'src')); // No subName given, or one Divi's metadata never names: unknown, never guessed. self::assertSame('unknown', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent')); self::assertSame('unknown', $catalog->pathKind('divi/blurb', 'imageIcon.innerContent', 'noSuchField')); // fullwidth-header's scrollDown.decoration.icon: show (toggle), color (color-picker), // size (range) – same composite pattern one level deeper. self::assertSame( ['show' => 'onoff', 'color' => 'color', 'size' => 'number'], $catalog->kinds('divi/fullwidth-header')['scrollDown.decoration.icon'] ); // buttonOne/buttonTwo.innerContent: both sub-fields use the same generic "divi/text" // field component, but "linkUrl" is a URL, not plain text – only Divi's own // features.dynamicContent.type tells them apart. foreach (['buttonOne', 'buttonTwo'] as $attrName) { self::assertSame('text', $catalog->pathKind('divi/fullwidth-header', $attrName . '.innerContent', 'text')); self::assertSame('url', $catalog->pathKind('divi/fullwidth-header', $attrName . '.innerContent', 'linkUrl')); } // image.innerContent only has an attrName+subName for "src" – the value actually // carries more keys at runtime (alt, id, titleText, synced by the upload component), // none of which Divi's metadata names on their own, so they stay unknown rather than // inheriting "image". self::assertSame(['src' => 'image'], $catalog->kinds('divi/fullwidth-header')['image.innerContent']); self::assertSame('image', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent', 'src')); self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent', 'alt')); self::assertSame('unknown', $catalog->pathKind('divi/fullwidth-header', 'image.innerContent')); // A plain (non-composite) path is unaffected: still a flat string, not a map. self::assertSame('richtext', $catalog->pathKind('divi/text', 'content.innerContent')); self::assertIsString($catalog->kinds('divi/text')['content.innerContent']); } }