From 4dba2c65fd3b054d3f2074851f885134c1fae801 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 6 Jul 2026 19:37:40 -0400 Subject: [PATCH] Normalize button diagnostic style shorthands --- .../ButtonMenuVisualProbeComparator.php | 136 +++++++++++++++++- .../unit/button-visual-probe-diagnostics.php | 12 ++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/php-transformer/src/VisualParity/ButtonMenuVisualProbeComparator.php b/php-transformer/src/VisualParity/ButtonMenuVisualProbeComparator.php index ca788ab7..3bf3fc43 100644 --- a/php-transformer/src/VisualParity/ButtonMenuVisualProbeComparator.php +++ b/php-transformer/src/VisualParity/ButtonMenuVisualProbeComparator.php @@ -364,7 +364,7 @@ private function wrapperChromeDelta(array $sourceProbe, array $targetProbe): arr private function wrapperChromeStyle(array $probe): array { $style = $probe['wrapper_chrome']['style'] ?? array(); - return is_array($style) ? array_filter($style, 'is_string') : array(); + return is_array($style) ? $this->canonicalComparableStyle(array_filter($style, 'is_string')) : array(); } /** @@ -435,7 +435,16 @@ private function styleGroupValues(array $style, string $group, array $fields): a return $values; } - return array_filter($values, static function (string $value, string $property): bool { + return $this->borderValues($values); + } + + /** + * @param array $values + * @return array + */ + private function borderValues(array $values): array + { + $values = array_filter($values, static function (string $value, string $property): bool { $normalized = strtolower(trim($value)); if ( '' === $normalized ) { return false; @@ -455,6 +464,129 @@ private function styleGroupValues(array $style, string $group, array $fields): a return true; }, ARRAY_FILTER_USE_BOTH); + + $shorthand = $this->parseBorderShorthand((string) ($values['border'] ?? '')); + if ( array() !== $shorthand ) { + $values = $shorthand + $values; + } + + foreach ( array('width', 'style', 'color') as $part ) { + $property = 'border-' . $part; + $sideValues = array(); + foreach ( array('top', 'right', 'bottom', 'left') as $side ) { + $sideProperty = 'border-' . $side . '-' . $part; + if ( isset($values[$sideProperty]) ) { + $sideValues[] = $values[$sideProperty]; + } + } + if ( 4 === count($sideValues) && 1 === count(array_unique($sideValues)) ) { + $values[$property] = $sideValues[0]; + } + } + + unset( + $values['border'], + $values['border-top-color'], + $values['border-top-style'], + $values['border-top-width'], + $values['border-right-color'], + $values['border-right-style'], + $values['border-right-width'], + $values['border-bottom-color'], + $values['border-bottom-style'], + $values['border-bottom-width'], + $values['border-left-color'], + $values['border-left-style'], + $values['border-left-width'] + ); + + ksort($values); + + return $values; + } + + /** + * @return array + */ + private function parseBorderShorthand(string $value): array + { + $value = trim($value); + if ( '' === $value || preg_match('/^(?:none|0(?:\.0+)?(?:px|rem|em)?)(?:\s+none)?$/i', $value) ) { + return array(); + } + + $parts = $this->splitCssValue($value); + $parsed = array(); + $color = array(); + foreach ( $parts as $part ) { + $normalized = strtolower($part); + if ( ! isset($parsed['border-width']) && preg_match('/^(?:thin|medium|thick|\d*\.?\d+(?:px|em|rem|%|vh|vw)?)$/i', $part) ) { + $parsed['border-width'] = $part; + continue; + } + if ( ! isset($parsed['border-style']) && in_array($normalized, array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'), true) ) { + $parsed['border-style'] = $part; + continue; + } + $color[] = $part; + } + + if ( array() !== $color ) { + $parsed['border-color'] = implode(' ', $color); + } + + return $parsed; + } + + /** + * @return array + */ + private function splitCssValue(string $value): array + { + $parts = array(); + $buffer = ''; + $depth = 0; + $length = strlen($value); + for ( $i = 0; $i < $length; $i++ ) { + $char = $value[$i]; + if ( '(' === $char ) { + ++$depth; + } elseif ( ')' === $char && $depth > 0 ) { + --$depth; + } + if ( 0 === $depth && ctype_space($char) ) { + if ( '' !== trim($buffer) ) { + $parts[] = trim($buffer); + $buffer = ''; + } + continue; + } + $buffer .= $char; + } + + if ( '' !== trim($buffer) ) { + $parts[] = trim($buffer); + } + + return $parts; + } + + /** + * @param array $style + * @return array + */ + private function canonicalComparableStyle(array $style): array + { + $canonical = array(); + foreach ( self::STYLE_GROUPS as $group => $fields ) { + foreach ( $this->styleGroupValues($style, $group, $fields) as $property => $value ) { + $canonical[$group . ':' . $property] = $value; + } + } + + ksort($canonical); + + return $canonical; } /** diff --git a/php-transformer/tests/unit/button-visual-probe-diagnostics.php b/php-transformer/tests/unit/button-visual-probe-diagnostics.php index f2129880..0f425918 100644 --- a/php-transformer/tests/unit/button-visual-probe-diagnostics.php +++ b/php-transformer/tests/unit/button-visual-probe-diagnostics.php @@ -112,6 +112,18 @@ ); $assert(in_array('button_shadow_missing', $causeCodes($shadowReport), true), 'reports missing button shadow/glow', json_encode($shadowReport['matches'][0] ?? array())); +$borderLonghandReport = $compare( + 'Start', + 'Start' +); +$assert(! in_array('button_border_mismatch', $causeCodes($borderLonghandReport), true), 'treats equivalent border shorthand and longhand as matching', json_encode($borderLonghandReport['matches'][0] ?? array())); + +$wrapperLonghandReport = $compare( + '', + '' +); +$assert(! in_array('button_wrapper_chrome_mismatch', $causeCodes($wrapperLonghandReport), true), 'treats equivalent wrapper chrome shorthand and longhand as matching', json_encode($wrapperLonghandReport['matches'][0] ?? array())); + if ( $failures > 0 ) { fwrite(STDERR, "Button visual probe diagnostic tests: {$failures} failed, {$passes} passed\n"); exit(1);