fix: Task 6 review round 1 - admin label overlap, heading default level, structure deny-list, row widths (S41)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 15:26:12 +02:00
co-authored by Claude Opus 5.5
parent 22bb1528db
commit 909ac82594
7 changed files with 294 additions and 11 deletions
@@ -510,5 +510,24 @@ final class AttrWriterTest extends TestCase
self::assertSame($reference, $deep['attrs']['button']['innerContent']['desktop']['value']['text']);
}
}
}
/** Ruling S41: structure attributes are refused whatever the catalog says (also for admins, also null). */
public function testStructureAttributesAreRefused(): void
{
foreach ([
['divi/section', 'module.advanced.type', ['desktop' => 'specialty']],
['divi/section', 'module.advanced.type', null],
['divi/section', 'module.advanced', ['desktop' => ['type' => 'fullwidth']]],
['divi/column', 'module.advanced.specialtyColumns', ['desktop' => '2']],
] as [$module, $path, $map]) {
$result = ComposeFixture::attrWriter(true)->apply($module, [], [$path => $map], 'attrs');
self::assertStringContainsString('Struktur', $result['errors']['attrs.' . $path] ?? '', $module . ' ' . $path);
self::assertSame([], $result['invalid_paths'], $path);
self::assertSame([], $result['attrs'], $path);
}
$heading = ComposeFixture::attrWriter()->apply('divi/heading', [], ['module.meta.adminLabel' => ['desktop' => 'x']], 'attrs');
self::assertSame([], $heading['errors'], 'only section type and specialty columns are structure');
}
}
@@ -231,6 +231,72 @@ final class ComposerTest extends TestCase
self::assertSame([], $separate['errors'], 'different paths, and the default heading level, may be combined');
}
/** Ruling S41: an admin label given in attrs is kept (no auto label over it); label + attrs on it is an error. */
public function testAdminLabelInAttrs(): void
{
$kept = ComposeFixture::composer()->compose([[
'type' => 'heading',
'text' => 'x',
'attrs' => ['module.meta.adminLabel' => ['desktop' => 'Mein Label', 'tablet' => 'Tablet']],
]], Block::create('divi/column'), 'nodes', 'Hero');
self::assertSame([], $kept['errors']);
self::assertSame(
['desktop' => ['value' => 'Mein Label'], 'tablet' => ['value' => 'Tablet']],
$kept['blocks'][0]->attrs()['module']['meta']['adminLabel']
);
$both = ComposeFixture::composer()->compose([[
'type' => 'heading',
'text' => 'x',
'label' => 'B',
'attrs' => ['module.meta.adminLabel' => ['desktop' => 'A', 'tablet' => 'T']],
]], Block::create('divi/column'), 'nodes', 'Hero');
self::assertStringContainsString('label', $both['errors']['nodes[0].attrs.module.meta.adminLabel']);
$update = ComposeFixture::composer()->update(Block::create('divi/heading'), [
'label' => 'B',
'attrs' => ['module.meta.adminLabel' => ['tablet' => 'T']],
], 'changes[0]');
self::assertStringContainsString('label', $update['errors']['changes[0].attrs.module.meta.adminLabel']);
}
/** Ruling S41: the default heading level survives attrs on the same font value. */
public function testDefaultHeadingLevelIsMergedIntoAttrs(): void
{
$result = ComposeFixture::composer()->compose([[
'type' => 'heading',
'text' => 'x',
'attrs' => ['title.decoration.font.font' => ['desktop' => ['color' => '#111111']]],
]], Block::create('divi/column'), 'nodes');
self::assertSame([], $result['errors']);
self::assertSame(
['headingLevel' => 'h2', 'color' => '#111111'],
$result['blocks'][0]->attrs()['title']['decoration']['font']['font']['desktop']['value']
);
$explicit = ComposeFixture::composer()->compose([[
'type' => 'heading',
'text' => 'x',
'attrs' => ['title.decoration.font.font' => ['desktop' => ['headingLevel' => 'h3']]],
]], Block::create('divi/column'), 'nodes');
self::assertSame('h3', $explicit['blocks'][0]->attrs()['title']['decoration']['font']['font']['desktop']['value']['headingLevel']);
}
/** Ruling S41: column widths on update follow the rules of "columns". */
public function testRowWidthsAreChecked(): void
{
$composer = ComposeFixture::composer();
self::assertNull($composer->rowWidthsError(['8_24', '16_24']));
self::assertNull($composer->rowWidthsError(['1_5', '1_5', '1_5', '1_5', '1_5']));
self::assertNotNull($composer->rowWidthsError(['12_24', '16_24']));
self::assertNotNull($composer->rowWidthsError(['1_13', '12_13']));
self::assertNotNull($composer->rowWidthsError(['x', '24_24']));
self::assertNotNull($composer->rowWidthsError([]));
}
/** Ruling S13: labels go through the choke point; a label that fails is an error, never stored raw. */
public function testLabelsAreCleanedOrReported(): void
{