From 0a4202bff48a7e1783a9ae69d9d2fe7f8a6720a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 23:59:30 -0400 Subject: [PATCH] test(ui): deepen attribute parity (name normalization, omission, styles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toward React-suite equivalency for ReactDOMServerIntegrationAttributes: - className->class and htmlFor->for name normalization - internal/framework props never emitted as attributes (key, children, event handlers — React's ref/children/dangerouslySetInnerHTML/suppress*Warning analog) - style maps serialize to a deterministic sorted k:v;k:v string - numeric (incl. zero) stringify; boolean true is a bare attr, false is omitted (React's array/object/symbol prop-coercion cases are N/A under Go static typing.) All pass; no version bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- ui/attribute_normalization_test.go | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 ui/attribute_normalization_test.go diff --git a/ui/attribute_normalization_test.go b/ui/attribute_normalization_test.go new file mode 100644 index 00000000..c29cc55e --- /dev/null +++ b/ui/attribute_normalization_test.go @@ -0,0 +1,76 @@ +//go:build !js || !wasm + +package ui_test + +import ( + "testing" + + "github.com/monstercameron/GoWebComponents/html" + "github.com/monstercameron/GoWebComponents/ui" +) + +func rawDiv(parseRaw map[string]any) string { + parseOut, _ := ui.RenderToString(html.Tag("div", html.Props{Raw: parseRaw}, ui.Text("x"))) + return parseOut +} + +// Deepens the ReactDOMServerIntegrationAttributes port toward equivalency: the +// React-prop -> HTML-attribute name normalizations and the props that must never +// be emitted as attributes. +func TestAttributeNameNormalization(t *testing.T) { + if parseGot := rawDiv(map[string]any{"className": "c1"}); parseGot != `
x
` { + t.Errorf("className->class: %q", parseGot) + } + parseLabel, _ := ui.RenderToString(html.Tag("label", html.Props{Raw: map[string]any{"htmlFor": "id1"}}, ui.Text("x"))) + if parseLabel != `` { + t.Errorf("htmlFor->for: %q", parseLabel) + } +} + +// Internal/framework props are never serialized as attributes (React: ref, key, +// children, dangerouslySetInnerHTML, suppress*Warning; GWC: key, children, event +// handlers, the DOM-ref sink). +func TestNonAttributePropsAreNotEmitted(t *testing.T) { + cases := []struct { + name string + raw map[string]any + }{ + {"key", map[string]any{"key": "k", "id": "real"}}, + {"children", map[string]any{"children": "nope", "id": "real"}}, + {"onClick", map[string]any{"onClick": func() {}, "id": "real"}}, + {"onclick-lower", map[string]any{"onclick": func() {}, "id": "real"}}, + } + for _, parseCase := range cases { + if parseGot := rawDiv(parseCase.raw); parseGot != `
x
` { + t.Errorf("%s should be omitted: %q", parseCase.name, parseGot) + } + } +} + +// Style maps serialize to a deterministic, sorted "k:v;k:v" string (React sorts +// for stable SSR output). +func TestStyleMapSerialization(t *testing.T) { + parseOut, _ := ui.RenderToString(html.Div(html.Props{ + Style: map[string]string{"margin": "0", "color": "red", "z-index": "2"}, + }, ui.Text("x"))) + if parseOut != `
x
` { + t.Errorf("style map = %q, want sorted color;margin;z-index", parseOut) + } +} + +// Numeric and boolean attribute values (via Raw): a number stringifies, a true +// boolean is a bare attribute, a false boolean is omitted. +func TestNumericAndBooleanAttributeValues(t *testing.T) { + if parseGot := rawDiv(map[string]any{"tabindex": 0}); parseGot != `
x
` { + t.Errorf("numeric zero: %q", parseGot) + } + if parseGot := rawDiv(map[string]any{"data-n": 42}); parseGot != `
x
` { + t.Errorf("numeric: %q", parseGot) + } + if parseGot := rawDiv(map[string]any{"hidden": true}); parseGot != `` { + t.Errorf("bool true: %q", parseGot) + } + if parseGot := rawDiv(map[string]any{"hidden": false}); parseGot != `
x
` { + t.Errorf("bool false should be omitted: %q", parseGot) + } +}