go-html/codegen/codegen_test.go
Snider 6369ec4405
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
fix(codegen): align custom element validation with spec
Broaden valid custom element names to match the HTML standard, add JS/TS string escaping for generated output, and cover dotted plus Unicode tag names in tests.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-15 02:37:39 +01:00

194 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build !js
package codegen
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateClass_ValidTag_Good(t *testing.T) {
js, err := GenerateClass("photo-grid", "C")
require.NoError(t, err)
assert.Contains(t, js, "class PhotoGrid extends HTMLElement")
assert.Contains(t, js, "attachShadow")
assert.Contains(t, js, `mode: "closed"`)
assert.Contains(t, js, "photo-grid")
}
func TestGenerateClass_InvalidTag_Bad(t *testing.T) {
_, err := GenerateClass("invalid", "C")
assert.Error(t, err, "custom element names must contain a hyphen")
_, err = GenerateClass("Nav-Bar", "C")
assert.Error(t, err, "custom element names must be lowercase")
_, err = GenerateClass("nav bar", "C")
assert.Error(t, err, "custom element names must reject spaces")
_, err = GenerateClass("annotation-xml", "C")
assert.Error(t, err, "reserved custom element names must be rejected")
}
func TestGenerateRegistration_DefinesCustomElement_Good(t *testing.T) {
js := GenerateRegistration("photo-grid", "PhotoGrid")
assert.Contains(t, js, "customElements.define")
assert.Contains(t, js, `"photo-grid"`)
assert.Contains(t, js, "PhotoGrid")
}
func TestGenerateClass_ValidExtendedTag_Good(t *testing.T) {
tests := []struct {
tag string
wantClass string
}{
{tag: "foo.bar-baz", wantClass: "FooBarBaz"},
{tag: "math-α", wantClass: "MathΑ"},
}
for _, tt := range tests {
t.Run(tt.tag, func(t *testing.T) {
js, err := GenerateClass(tt.tag, "C")
require.NoError(t, err)
assert.Contains(t, js, "class "+tt.wantClass+" extends HTMLElement")
assert.Contains(t, js, `tag: "`+tt.tag+`"`)
assert.Contains(t, js, `slot = this.getAttribute("data-slot") || "C";`)
})
}
}
func TestTagToClassName_KebabCase_Good(t *testing.T) {
tests := []struct{ tag, want string }{
{"photo-grid", "PhotoGrid"},
{"nav-breadcrumb", "NavBreadcrumb"},
{"my-super-widget", "MySuperWidget"},
{"nav_bar", "NavBar"},
{"nav.bar", "NavBar"},
{"nav--bar", "NavBar"},
{"math-α", "MathΑ"},
}
for _, tt := range tests {
got := TagToClassName(tt.tag)
assert.Equal(t, tt.want, got, "TagToClassName(%q)", tt.tag)
}
}
func TestGenerateBundle_DeduplicatesRegistrations_Good(t *testing.T) {
slots := map[string]string{
"H": "nav-bar",
"C": "main-content",
"F": "nav-bar",
}
js, err := GenerateBundle(slots)
require.NoError(t, err)
assert.Contains(t, js, "NavBar")
assert.Contains(t, js, "MainContent")
assert.Equal(t, 2, countSubstr(js, "extends HTMLElement"))
assert.Equal(t, 2, countSubstr(js, "customElements.define"))
}
func TestGenerateBundle_DeterministicOrdering_Good(t *testing.T) {
slots := map[string]string{
"Z": "zed-panel",
"A": "alpha-panel",
"M": "main-content",
}
js, err := GenerateBundle(slots)
require.NoError(t, err)
alpha := strings.Index(js, "class AlphaPanel")
main := strings.Index(js, "class MainContent")
zed := strings.Index(js, "class ZedPanel")
assert.NotEqual(t, -1, alpha)
assert.NotEqual(t, -1, main)
assert.NotEqual(t, -1, zed)
assert.Less(t, alpha, main)
assert.Less(t, main, zed)
assert.Equal(t, 3, countSubstr(js, "extends HTMLElement"))
assert.Equal(t, 3, countSubstr(js, "customElements.define"))
}
func TestGenerateTypeScriptDefinitions_DeduplicatesAndOrders_Good(t *testing.T) {
slots := map[string]string{
"Z": "zed-panel",
"A": "alpha-panel",
"M": "alpha-panel",
}
dts := GenerateTypeScriptDefinitions(slots)
assert.Contains(t, dts, `interface HTMLElementTagNameMap`)
assert.Contains(t, dts, `"alpha-panel": AlphaPanel;`)
assert.Contains(t, dts, `"zed-panel": ZedPanel;`)
assert.Equal(t, 1, countSubstr(dts, `"alpha-panel": AlphaPanel;`))
assert.Equal(t, 1, countSubstr(dts, `export declare class AlphaPanel extends HTMLElement`))
assert.Equal(t, 1, countSubstr(dts, `export declare class ZedPanel extends HTMLElement`))
assert.Contains(t, dts, "export {};")
assert.Less(t, strings.Index(dts, `"alpha-panel": AlphaPanel;`), strings.Index(dts, `"zed-panel": ZedPanel;`))
}
func TestGenerateTypeScriptDefinitions_SkipsInvalidTags_Good(t *testing.T) {
slots := map[string]string{
"H": "nav-bar",
"C": "Nav-Bar",
"F": "nav bar",
}
dts := GenerateTypeScriptDefinitions(slots)
assert.Contains(t, dts, `"nav-bar": NavBar;`)
assert.NotContains(t, dts, "Nav-Bar")
assert.NotContains(t, dts, "nav bar")
assert.Equal(t, 1, countSubstr(dts, `export declare class NavBar extends HTMLElement`))
}
func TestGenerateTypeScriptDefinitions_ValidExtendedTag_Good(t *testing.T) {
slots := map[string]string{
"H": "foo.bar-baz",
}
dts := GenerateTypeScriptDefinitions(slots)
assert.Contains(t, dts, `"foo.bar-baz": FooBarBaz;`)
assert.Contains(t, dts, `export declare class FooBarBaz extends HTMLElement`)
}
func countSubstr(s, substr string) int {
if substr == "" {
return len(s) + 1
}
count := 0
for i := 0; i <= len(s)-len(substr); {
j := indexSubstr(s[i:], substr)
if j < 0 {
return count
}
count++
i += j + len(substr)
}
return count
}
func indexSubstr(s, substr string) int {
if substr == "" {
return 0
}
if len(substr) > len(s) {
return -1
}
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}