From 852bd52c49e2fa44aebb1cbeefc7aa6d16672af0 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 17 Aug 2026 01:57:57 -0700 Subject: [PATCH 1/8] test(transformer): reproduce the branding anchor folded into a promoted navigation A nav landmark that authors a branding anchor beside a menu list collapses into one core/navigation with the brand as an extra menu item. The deferral guard only recognises a brand anchor whose class matches a hard-coded vocabulary, so a class outside that list leaves the collapse in place. Two consequences ride along: the brand emits `anchorClassName`, which is not a registered core/navigation-link attribute, and the menu list's className is copied onto the nav container where it outranks the landmark's own padding rule. Red first: the unit contract reports 10 failed / 7 passed and the parity fixture fails at `blocks.0.innerBlocks` with 4 children instead of 2. Covers gates N3 and N4. --- php-transformer/composer.json | 1 + .../parity/html-nav-brand-anchor-hoist.json | 104 +++++++++ .../unit/navigation-brand-anchor-hoist.php | 198 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 php-transformer/tests/fixtures/parity/html-nav-brand-anchor-hoist.json create mode 100644 php-transformer/tests/unit/navigation-brand-anchor-hoist.php diff --git a/php-transformer/composer.json b/php-transformer/composer.json index a49b7213..18c1cb43 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -85,6 +85,7 @@ "php tests/unit/artifact-author-stylesheet-projection.php", "php tests/unit/fallback-finding-normalizer.php", "php tests/unit/navigation-underline-color-resolver.php", + "php tests/unit/navigation-brand-anchor-hoist.php", "php tests/unit/button-signal-classifier.php", "php tests/unit/button-style-resolver.php", "php tests/unit/button-font-family-carry.php", diff --git a/php-transformer/tests/fixtures/parity/html-nav-brand-anchor-hoist.json b/php-transformer/tests/fixtures/parity/html-nav-brand-anchor-hoist.json new file mode 100644 index 00000000..8fd080a3 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-nav-brand-anchor-hoist.json @@ -0,0 +1,104 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-nav-brand-anchor-hoist", + "description": "A nav landmark that authors a branding anchor beside a menu list keeps three distinct elements: the landmark becomes a core/group{tagName:nav} carrier holding the brand block and the promoted core/navigation, so the brand is never folded in as a menu item. Detection is structural — a direct-child anchor outside the link cluster — so a brand class outside any vocabulary allowlist is still recognised. Folding the brand in emitted the unregistered anchorClassName attribute on core/navigation-link, copied the menu list's className onto the nav container, and blocked the shared link colour from being promoted to the navigation block.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-nav-brand-anchor-hoist.json", + "notes": "Product-neutral header shape: nav landmark with a direct branding anchor whose class carries no brand/logo token, beside a ul menu whose class carries no navigation token." + }, + "legacy_comparison": { + "skip": true, + "reason": "This fixture covers native block decomposition fidelity beyond the legacy converter." + }, + "operation": "html_transformer.transform", + "input": { + "content": "", + "options": { + "static_css": ".wordmark{display:flex;flex-direction:column;line-height:1}.wordmark .place{font-size:.6rem;letter-spacing:.16em;text-transform:uppercase;color:#A9B4C2}.navlinks{list-style:none;margin:0;padding:0;display:flex;gap:14px 22px}.navlinks a{letter-spacing:.16em;text-transform:uppercase;color:#DDE3EB}" + } + }, + "expected_blocks": [ + { + "path": "blocks.0", + "name": "core/group", + "attrs": { + "tagName": "nav" + } + }, + { + "path": "blocks.0.innerBlocks.0", + "name": "core/paragraph" + }, + { + "path": "blocks.0.innerBlocks.1", + "name": "core/navigation", + "attrs": { + "className": "navlinks blocks-engine-list-navigation", + "overlayMenu": "mobile", + "customTextColor": "#DDE3EB" + } + }, + { + "path": "blocks.0.innerBlocks.1.innerBlocks.0", + "name": "core/navigation-link", + "attrs": { + "label": "Home", + "url": "/", + "kind": "custom" + } + }, + { + "path": "blocks.0.innerBlocks.1.innerBlocks.2", + "name": "core/navigation-link", + "attrs": { + "label": "Contact", + "url": "/contact/", + "kind": "custom" + } + } + ], + "expected_fallbacks": [], + "expect": [ + { + "path": "status", + "assert": "equals", + "value": "success" + }, + { + "path": "blocks", + "assert": "count", + "count": 1 + }, + { + "path": "blocks.0.innerBlocks", + "assert": "count", + "count": 2 + }, + { + "path": "blocks.0.innerBlocks.1.innerBlocks", + "assert": "count", + "count": 3 + }, + { + "path": "serialized_blocks", + "assert": "not_contains", + "value": "anchorClassName" + }, + { + "path": "serialized_blocks", + "assert": "contains", + "value": "" + }, + { + "path": "serialized_blocks", + "assert": "not_contains", + "value": ").)*Camden/s', $unlistedMarkup), + 'unlisted brand class: the branding anchor is not folded in as a menu item label', + $unlistedMarkup +); +$assert( + str_contains($unlistedMarkup, ''), + 'unlisted brand class: the branding anchor keeps its own class so its authored layout still applies', + $unlistedMarkup +); +$assert( + 0 === count($findBlocks($unlistedBlocks, 'core/html')), + 'unlisted brand class: the hoisted brand is a real block rather than an HTML fallback', + $unlistedMarkup +); +$assert( + '#DDE3EB' === (string) ($unlistedNavigations[0]['attrs']['customTextColor'] ?? ''), + 'unlisted brand class: the colour every remaining link shares is promoted onto the navigation', + json_encode($unlistedNavigations[0]['attrs'] ?? array()) +); + +// -- N4: the menu list's className must not land on the nav container, where it +// would outrank `header nav` and zero the header's padding. +$assert( + array() !== $unlistedNavGroups && ! in_array('navlinks', $classTokens($unlistedNavGroups[0]), true), + 'promoted nav className: the list class is not copied onto the nav container element', + json_encode($unlistedNavGroups[0]['attrs'] ?? array()) +); +$assert( + in_array('navlinks', $classTokens($unlistedNavigations[0]), true), + 'promoted nav className: the list class moves to the block that stands in for the list', + json_encode($unlistedNavigations[0]['attrs'] ?? array()) +); +$assert( + 1 === preg_match('/