From 679ed8936f023df39fc6bcaf33e88f158968a768 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sun, 16 Aug 2026 10:03:39 -0700 Subject: [PATCH 1/4] test(php-transformer): freeze auto-fit grid and css-owned flex contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds two deliberately RED contracts, one per confirmed defect: `repeat(auto-fit, …)` track lists must not be converted to a native WordPress grid layout attribute, and a flex container demoted to a css-owned core/group must keep its authored `display:flex`. ## Why `StyleResolutionTrait::autoRepeatMinimumColumnWidth()` accepts both `auto-fit` and `auto-fill` and converts both to `layout.minimumColumnWidth`. `wp-includes/block-supports/layout.php` hardcodes `auto-fill` in every branch that renders that attribute, so an authored `auto-fit` grid renders with empty tracks retained and the content crammed into part of the measure. `cssOwnedGroupAttributes()` drops the native `layout` attribute on demotion. Grids survive because `CSS_OWNED_GRID_CARRIER_PROPERTIES` rides their inline declarations to the generated stylesheet; there is no flex equivalent, so the block is marked `blocks-engine-css-owned-flow` while the declaration that makes it a flex container is dropped and the children stack. ## How `tests/unit/auto-fit-grid-carrier.php` covers inline and class-owned `auto-fit` track lists plus `auto-fill` controls proving the native conversion is retained, not abandoned. `tests/unit/css-owned-flex-carrier.php` covers inline row and column flex containers plus controls for a container with no authored display and for a class-owned flex declaration the author stylesheet already owns. ## Testing RED evidence at this commit: `php tests/unit/auto-fit-grid-carrier.php` exits 1 with 6 failed and 5 passed assertions; `php tests/unit/css-owned-flex-carrier.php` exits 1 with 6 failed and 6 passed assertions. --- php-transformer/composer.json | 2 + .../tests/unit/auto-fit-grid-carrier.php | 141 ++++++++++++++++ .../tests/unit/css-owned-flex-carrier.php | 154 ++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 php-transformer/tests/unit/auto-fit-grid-carrier.php create mode 100644 php-transformer/tests/unit/css-owned-flex-carrier.php diff --git a/php-transformer/composer.json b/php-transformer/composer.json index 7d7a140e..5a8a61a0 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -80,6 +80,8 @@ "php tests/unit/engine-support-css-asset.php", "php tests/unit/engine-support-css-specificity.php", "php tests/unit/inline-display-carrier.php", + "php tests/unit/css-owned-flex-carrier.php", + "php tests/unit/auto-fit-grid-carrier.php", "php tests/unit/artifact-author-stylesheet-projection.php", "php tests/unit/fallback-finding-normalizer.php", "php tests/unit/navigation-underline-color-resolver.php", diff --git a/php-transformer/tests/unit/auto-fit-grid-carrier.php b/php-transformer/tests/unit/auto-fit-grid-carrier.php new file mode 100644 index 00000000..14641c4e --- /dev/null +++ b/php-transformer/tests/unit/auto-fit-grid-carrier.php @@ -0,0 +1,141 @@ + (string) ($asset['content'] ?? ''), + array_values(array_filter( + is_array($result['assets'] ?? null) ? $result['assets'] : array(), + static fn (array $asset): bool => $source === ($asset['source'] ?? '') + )) + )); +}; + +$transform = static fn (string $html): array => ( new HtmlTransformer() )->transform($html, array())->toArray(); + +$cards = '

One

Two

Three

'; + +// -- Defect: an inline auto-fit track list is converted to a native grid layout. +$inlineAutoFit = $transform( + '
' . $cards . '
' +); +$inlineAutoFitMarkup = (string) ($inlineAutoFit['serialized_blocks'] ?? ''); +$inlineAutoFitEngineCss = $cssFor($inlineAutoFit, 'engine-support'); + +$assert( + ! str_contains($inlineAutoFitMarkup, 'minimumColumnWidth'), + 'inline auto-fit: no native grid layout attribute, which WordPress would render as auto-fill', + $inlineAutoFitMarkup +); +$assert( + str_contains($inlineAutoFitMarkup, 'blocks-engine-css-owned-grid'), + 'inline auto-fit: container is marked css-owned-grid like every other non-expressible track list', + $inlineAutoFitMarkup +); +$assert( + str_contains($inlineAutoFitEngineCss, 'grid-template-columns:repeat(auto-fit, minmax(280px, 1fr))'), + 'inline auto-fit: the authored track list rides to the generated stylesheet verbatim', + $inlineAutoFitEngineCss +); +$assert( + str_contains($inlineAutoFitEngineCss, 'display:grid'), + 'inline auto-fit: the carrier keeps the container a grid', + $inlineAutoFitEngineCss +); + +// -- Same defect through a class-owned track list, which is the shape the +// site-builder corpus authors. The author stylesheet already retains the rule; +// the native attribute is what overrides it. +$classAutoFit = $transform( + '' + . '
' . $cards . '
' +); +$classAutoFitMarkup = (string) ($classAutoFit['serialized_blocks'] ?? ''); +$classAutoFitAuthorCss = $cssFor($classAutoFit, 'author-css'); + +$assert( + ! str_contains($classAutoFitMarkup, 'minimumColumnWidth'), + 'class-owned auto-fit: no native grid layout attribute', + $classAutoFitMarkup +); +$assert( + ! str_contains($classAutoFitMarkup, 'is-layout-grid'), + 'class-owned auto-fit: no core grid layout classes competing with the author rule', + $classAutoFitMarkup +); +$assert( + str_contains($classAutoFitAuthorCss, '.cards{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:24px}'), + 'class-owned auto-fit: the author rule stays the single owner of the track geometry', + $classAutoFitAuthorCss +); + +// -- Control: auto-fill IS natively expressible and must keep converting. +$inlineAutoFill = $transform( + '
' . $cards . '
' +); +$inlineAutoFillMarkup = (string) ($inlineAutoFill['serialized_blocks'] ?? ''); +$inlineAutoFillEngineCss = $cssFor($inlineAutoFill, 'engine-support'); + +$assert( + str_contains($inlineAutoFillMarkup, '"layout":{"type":"grid","minimumColumnWidth":"280px"}'), + 'auto-fill control: still converts to the native grid layout attribute', + $inlineAutoFillMarkup +); +$assert( + ! str_contains($inlineAutoFillMarkup, 'blocks-engine-css-owned-grid'), + 'auto-fill control: does not fall back to the css-owned-grid carrier', + $inlineAutoFillMarkup +); +$assert( + ! str_contains($inlineAutoFillEngineCss, 'grid-template-columns'), + 'auto-fill control: WordPress still owns the track geometry', + $inlineAutoFillEngineCss +); + +$classAutoFill = $transform( + '' + . '
' . $cards . '
' +); +$classAutoFillMarkup = (string) ($classAutoFill['serialized_blocks'] ?? ''); + +$assert( + str_contains($classAutoFillMarkup, '"layout":{"type":"grid","minimumColumnWidth":"280px"}'), + 'class-owned auto-fill control: still converts to the native grid layout attribute', + $classAutoFillMarkup +); + +if ( $failures > 0 ) { + fwrite(STDERR, "Auto-fit grid carrier contract: {$failures} failed, {$passes} passed\n"); + exit(1); +} + +fwrite(STDOUT, "Auto-fit grid carrier contract passed: {$passes} assertions\n"); diff --git a/php-transformer/tests/unit/css-owned-flex-carrier.php b/php-transformer/tests/unit/css-owned-flex-carrier.php new file mode 100644 index 00000000..bfb33fb7 --- /dev/null +++ b/php-transformer/tests/unit/css-owned-flex-carrier.php @@ -0,0 +1,154 @@ + (string) ($asset['content'] ?? ''), + array_values(array_filter( + is_array($result['assets'] ?? null) ? $result['assets'] : array(), + static fn (array $asset): bool => $source === ($asset['source'] ?? '') + )) + )); +}; + +$transform = static fn (string $html): array => ( new HtmlTransformer() )->transform($html, array())->toArray(); + +/** Rule body for the geometry carrier class present in the markup. */ +$carrierRule = static function (string $markup, string $css): string { + if ( 1 !== preg_match('/\b(be-inline-geometry-[a-f0-9-]+)\b/', $markup, $match) ) { + return ''; + } + if ( 1 !== preg_match('/(?

Alpha

Beta

Gamma

'; + +// -- Defect: a css-owned-flow footer loses its inline display:flex entirely. +$flex = $transform( + '' +); +$flexMarkup = (string) ($flex['serialized_blocks'] ?? ''); +$flexEngineCss = $cssFor($flex, 'engine-support'); +$flexRule = $carrierRule($flexMarkup, $flexEngineCss); + +$assert( + str_contains($flexMarkup, 'blocks-engine-css-owned-flow'), + 'inline flex: the container is still demoted to a css-owned-flow group', + $flexMarkup +); +$assert( + 1 === preg_match('/\bbe-inline-geometry-[a-f0-9-]+\b/', $flexMarkup), + 'inline flex: the demoted container receives a geometry carrier class', + $flexMarkup +); +$assert( + str_contains($flexRule, 'display:flex'), + 'inline flex: the carrier keeps the container a flex container', + '' !== $flexRule ? $flexRule : $flexEngineCss +); +$assert( + str_contains($flexRule, 'gap:2rem'), + 'inline flex: the authored gap rides with the display declaration', + '' !== $flexRule ? $flexRule : $flexEngineCss +); +$assert( + str_contains($flexRule, 'justify-content:space-between'), + 'inline flex: the authored main-axis distribution rides with the display declaration', + '' !== $flexRule ? $flexRule : $flexEngineCss +); +$assert( + str_contains($flexRule, 'align-items:center'), + 'inline flex: the authored cross-axis alignment rides with the display declaration', + '' !== $flexRule ? $flexRule : $flexEngineCss +); + +// -- A vertical flex column keeps its direction rather than silently becoming a row. +$column = $transform( + '' +); +$columnMarkup = (string) ($column['serialized_blocks'] ?? ''); +$columnRule = $carrierRule($columnMarkup, $cssFor($column, 'engine-support')); + +$assert( + str_contains($columnRule, 'display:flex') && str_contains($columnRule, 'flex-direction:column') && str_contains($columnRule, 'flex-wrap:wrap'), + 'inline flex column: direction and wrap ride with the display declaration', + '' !== $columnRule ? $columnRule : $cssFor($column, 'engine-support') +); + +// -- Control: no authored display gains no carrier and no flex declaration. +$plain = $transform(''); +$plainMarkup = (string) ($plain['serialized_blocks'] ?? ''); +$plainEngineCss = $cssFor($plain, 'engine-support'); + +$assert( + 1 !== preg_match('/\bbe-inline-geometry-[a-f0-9-]+\b/', $plainMarkup), + 'no authored display control: no geometry carrier class is invented', + $plainMarkup +); +$assert( + ! str_contains($plainEngineCss, 'display:flex'), + 'no authored display control: no flex declaration is invented', + $plainEngineCss +); +$assert( + str_starts_with($plainMarkup, '