fix(conventions): isolate banned imports and clarify tests

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-26 11:15:24 +00:00
parent b3f622988d
commit 8a3f28aff3
13 changed files with 62 additions and 26 deletions

View file

@ -1,7 +1,7 @@
package html package html
import ( import (
"fmt" "strconv"
"testing" "testing"
i18n "dappco.re/go/core/i18n" i18n "dappco.re/go/core/i18n"
@ -100,7 +100,7 @@ func BenchmarkImprint_Small(b *testing.B) {
func BenchmarkImprint_Large(b *testing.B) { func BenchmarkImprint_Large(b *testing.B) {
items := make([]string, 20) items := make([]string, 20)
for i := range items { for i := range items {
items[i] = fmt.Sprintf("Item %d was created successfully", i) items[i] = "Item " + strconv.Itoa(i) + " was created successfully"
} }
page := NewLayout("HLCRF"). page := NewLayout("HLCRF").
H(El("h1", Text("Building project"))). H(El("h1", Text("Building project"))).
@ -207,7 +207,7 @@ func BenchmarkLayout_Nested(b *testing.B) {
func BenchmarkLayout_ManySlotChildren(b *testing.B) { func BenchmarkLayout_ManySlotChildren(b *testing.B) {
nodes := make([]Node, 50) nodes := make([]Node, 50)
for i := range nodes { for i := range nodes {
nodes[i] = El("p", Raw(fmt.Sprintf("paragraph %d", i))) nodes[i] = El("p", Raw("paragraph "+strconv.Itoa(i)))
} }
layout := NewLayout("HLCRF"). layout := NewLayout("HLCRF").
H(Raw("header")). H(Raw("header")).
@ -242,7 +242,7 @@ func benchEach(b *testing.B, n int) {
items[i] = i items[i] = i
} }
node := Each(items, func(i int) Node { node := Each(items, func(i int) Node {
return El("li", Raw(fmt.Sprintf("item-%d", i))) return El("li", Raw("item-"+strconv.Itoa(i)))
}) })
ctx := NewContext() ctx := NewContext()

View file

@ -1,3 +1,5 @@
//go:build !js
// Package main provides a build-time CLI for generating Web Component JS bundles. // Package main provides a build-time CLI for generating Web Component JS bundles.
// Reads a JSON slot map from stdin, writes the generated JS to stdout. // Reads a JSON slot map from stdin, writes the generated JS to stdout.
// //

View file

@ -1,3 +1,5 @@
//go:build !js
package main package main
import ( import (
@ -9,7 +11,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestRun_Good(t *testing.T) { func TestRun_WritesBundle(t *testing.T) {
input := strings.NewReader(`{"H":"nav-bar","C":"main-content"}`) input := strings.NewReader(`{"H":"nav-bar","C":"main-content"}`)
var output bytes.Buffer var output bytes.Buffer
@ -23,7 +25,7 @@ func TestRun_Good(t *testing.T) {
assert.Equal(t, 2, strings.Count(js, "extends HTMLElement")) assert.Equal(t, 2, strings.Count(js, "extends HTMLElement"))
} }
func TestRun_Bad_InvalidJSON(t *testing.T) { func TestRun_InvalidJSON(t *testing.T) {
input := strings.NewReader(`not json`) input := strings.NewReader(`not json`)
var output bytes.Buffer var output bytes.Buffer
@ -32,7 +34,7 @@ func TestRun_Bad_InvalidJSON(t *testing.T) {
assert.Contains(t, err.Error(), "invalid JSON") assert.Contains(t, err.Error(), "invalid JSON")
} }
func TestRun_Bad_InvalidTag(t *testing.T) { func TestRun_InvalidTag(t *testing.T) {
input := strings.NewReader(`{"H":"notag"}`) input := strings.NewReader(`{"H":"notag"}`)
var output bytes.Buffer var output bytes.Buffer
@ -41,7 +43,7 @@ func TestRun_Bad_InvalidTag(t *testing.T) {
assert.Contains(t, err.Error(), "hyphen") assert.Contains(t, err.Error(), "hyphen")
} }
func TestRun_Good_Empty(t *testing.T) { func TestRun_EmptySlots(t *testing.T) {
input := strings.NewReader(`{}`) input := strings.NewReader(`{}`)
var output bytes.Buffer var output bytes.Buffer

View file

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestBuildComponentJS_Good(t *testing.T) { func TestBuildComponentJS_ValidJSON(t *testing.T) {
slotsJSON := `{"H":"nav-bar","C":"main-content"}` slotsJSON := `{"H":"nav-bar","C":"main-content"}`
js, err := buildComponentJS(slotsJSON) js, err := buildComponentJS(slotsJSON)
require.NoError(t, err) require.NoError(t, err)
@ -18,7 +18,7 @@ func TestBuildComponentJS_Good(t *testing.T) {
assert.Contains(t, js, "customElements.define") assert.Contains(t, js, "customElements.define")
} }
func TestBuildComponentJS_Bad_InvalidJSON(t *testing.T) { func TestBuildComponentJS_InvalidJSON(t *testing.T) {
_, err := buildComponentJS("not json") _, err := buildComponentJS("not json")
assert.Error(t, err) assert.Error(t, err)
} }

View file

@ -21,7 +21,7 @@ const (
wasmRawLimit = 3_670_016 // 3.5 MB raw size limit wasmRawLimit = 3_670_016 // 3.5 MB raw size limit
) )
func TestWASMBinarySize_Good(t *testing.T) { func TestWASMBinarySize_WithinBudget(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping WASM build test in short mode") t.Skip("skipping WASM build test in short mode")
} }

View file

@ -1,3 +1,5 @@
//go:build !js
package codegen package codegen
import "testing" import "testing"

View file

@ -1,7 +1,8 @@
//go:build !js
package codegen package codegen
import ( import (
"fmt"
"strings" "strings"
"text/template" "text/template"
@ -51,7 +52,7 @@ func GenerateClass(tag, slot string) (string, error) {
// GenerateRegistration produces the customElements.define() call. // GenerateRegistration produces the customElements.define() call.
func GenerateRegistration(tag, className string) string { func GenerateRegistration(tag, className string) string {
return fmt.Sprintf(`customElements.define("%s", %s);`, tag, className) return `customElements.define("` + tag + `", ` + className + `);`
} }
// TagToClassName converts a kebab-case tag to PascalCase class name. // TagToClassName converts a kebab-case tag to PascalCase class name.

View file

@ -1,3 +1,5 @@
//go:build !js
package codegen package codegen
import ( import (
@ -8,7 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestGenerateClass_Good(t *testing.T) { func TestGenerateClass_ValidTag(t *testing.T) {
js, err := GenerateClass("photo-grid", "C") js, err := GenerateClass("photo-grid", "C")
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, js, "class PhotoGrid extends HTMLElement") assert.Contains(t, js, "class PhotoGrid extends HTMLElement")
@ -17,19 +19,19 @@ func TestGenerateClass_Good(t *testing.T) {
assert.Contains(t, js, "photo-grid") assert.Contains(t, js, "photo-grid")
} }
func TestGenerateClass_Bad_InvalidTag(t *testing.T) { func TestGenerateClass_InvalidTag(t *testing.T) {
_, err := GenerateClass("invalid", "C") _, err := GenerateClass("invalid", "C")
assert.Error(t, err, "custom element names must contain a hyphen") assert.Error(t, err, "custom element names must contain a hyphen")
} }
func TestGenerateRegistration_Good(t *testing.T) { func TestGenerateRegistration_DefinesCustomElement(t *testing.T) {
js := GenerateRegistration("photo-grid", "PhotoGrid") js := GenerateRegistration("photo-grid", "PhotoGrid")
assert.Contains(t, js, "customElements.define") assert.Contains(t, js, "customElements.define")
assert.Contains(t, js, `"photo-grid"`) assert.Contains(t, js, `"photo-grid"`)
assert.Contains(t, js, "PhotoGrid") assert.Contains(t, js, "PhotoGrid")
} }
func TestTagToClassName_Good(t *testing.T) { func TestTagToClassName_KebabCase(t *testing.T) {
tests := []struct{ tag, want string }{ tests := []struct{ tag, want string }{
{"photo-grid", "PhotoGrid"}, {"photo-grid", "PhotoGrid"},
{"nav-breadcrumb", "NavBreadcrumb"}, {"nav-breadcrumb", "NavBreadcrumb"},
@ -41,14 +43,16 @@ func TestTagToClassName_Good(t *testing.T) {
} }
} }
func TestGenerateBundle_Good(t *testing.T) { func TestGenerateBundle_DeduplicatesRegistrations(t *testing.T) {
slots := map[string]string{ slots := map[string]string{
"H": "nav-bar", "H": "nav-bar",
"C": "main-content", "C": "main-content",
"F": "nav-bar",
} }
js, err := GenerateBundle(slots) js, err := GenerateBundle(slots)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, js, "NavBar") assert.Contains(t, js, "NavBar")
assert.Contains(t, js, "MainContent") assert.Contains(t, js, "MainContent")
assert.Equal(t, 2, strings.Count(js, "extends HTMLElement")) assert.Equal(t, 2, strings.Count(js, "extends HTMLElement"))
assert.Equal(t, 2, strings.Count(js, "customElements.define"))
} }

13
codegen/doc.go Normal file
View file

@ -0,0 +1,13 @@
//go:build !js
// SPDX-Licence-Identifier: EUPL-1.2
// Package codegen generates Web Component bundles for go-html slot maps.
//
// Use it at build time, or through the cmd/codegen CLI:
//
// bundle, err := GenerateBundle(map[string]string{
// "H": "site-header",
// "C": "app-main",
// })
package codegen

12
doc.go Normal file
View file

@ -0,0 +1,12 @@
// SPDX-Licence-Identifier: EUPL-1.2
// Package html renders semantic HTML from composable node trees.
//
// A typical page combines Layout, El, Text, and Render:
//
// page := NewLayout("HCF").
// H(El("h1", Text("page.title"))).
// C(El("main", Text("page.body"))).
// F(El("small", Text("page.footer")))
// out := Render(page, NewContext())
package html

View file

@ -66,7 +66,7 @@ go test ./cmd/codegen/
go test ./cmd/wasm/ go test ./cmd/wasm/
``` ```
The WASM size gate test (`TestWASMBinarySize_Good`) builds the WASM binary as a subprocess. It is slow and is skipped under `-short`. It is also guarded with `//go:build !js` so it cannot run within the WASM environment itself. The WASM size gate test (`TestWASMBinarySize_WithinBudget`) builds the WASM binary as a subprocess. It is slow and is skipped under `-short`. It is also guarded with `//go:build !js` so it cannot run within the WASM environment itself.
### Test Dependencies ### Test Dependencies
@ -278,7 +278,7 @@ func TestIntegration_RenderThenReverse(t *testing.T) {
### Codegen Tests with Testify ### Codegen Tests with Testify
```go ```go
func TestGenerateClass_Good(t *testing.T) { func TestGenerateClass_ValidTag(t *testing.T) {
js, err := GenerateClass("photo-grid", "C") js, err := GenerateClass("photo-grid", "C")
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, js, "class PhotoGrid extends HTMLElement") assert.Contains(t, js, "class PhotoGrid extends HTMLElement")

View file

@ -78,7 +78,7 @@ The fix was applied in three distinct steps:
### Size gate test (`aae5d21`) ### Size gate test (`aae5d21`)
`cmd/wasm/size_test.go` was added to prevent regression. `TestWASMBinarySize_Good` builds the WASM binary in a temp directory, gzip-compresses it, and asserts: `cmd/wasm/size_test.go` was added to prevent regression. `TestWASMBinarySize_WithinBudget` builds the WASM binary in a temp directory, gzip-compresses it, and asserts:
- Gzip size < 1,048,576 bytes (1 MB). - Gzip size < 1,048,576 bytes (1 MB).
- Raw size < 3,145,728 bytes (3 MB). - Raw size < 3,145,728 bytes (3 MB).

View file

@ -1,7 +1,7 @@
package html package html
import ( import (
"fmt" "strconv"
"strings" "strings"
"testing" "testing"
@ -196,7 +196,7 @@ func TestLayout_DeepNesting_10Levels(t *testing.T) {
for i := 1; i < 10; i++ { for i := 1; i < 10; i++ {
expectedBlock += "-C-0" expectedBlock += "-C-0"
} }
if !strings.Contains(got, fmt.Sprintf(`data-block="%s"`, expectedBlock)) { if !strings.Contains(got, `data-block="`+expectedBlock+`"`) {
t.Errorf("10 levels deep: missing expected block ID %q in:\n%s", expectedBlock, got) t.Errorf("10 levels deep: missing expected block ID %q in:\n%s", expectedBlock, got)
} }
@ -251,7 +251,7 @@ func TestEach_LargeIteration_1000(t *testing.T) {
} }
node := Each(items, func(i int) Node { node := Each(items, func(i int) Node {
return El("li", Raw(fmt.Sprintf("%d", i))) return El("li", Raw(strconv.Itoa(i)))
}) })
got := node.Render(ctx) got := node.Render(ctx)
@ -275,7 +275,7 @@ func TestEach_LargeIteration_5000(t *testing.T) {
} }
node := Each(items, func(i int) Node { node := Each(items, func(i int) Node {
return El("span", Raw(fmt.Sprintf("%d", i))) return El("span", Raw(strconv.Itoa(i)))
}) })
got := node.Render(ctx) got := node.Render(ctx)
@ -292,7 +292,7 @@ func TestEach_NestedEach(t *testing.T) {
node := Each(rows, func(row int) Node { node := Each(rows, func(row int) Node {
return El("tr", Each(cols, func(col string) Node { return El("tr", Each(cols, func(col string) Node {
return El("td", Raw(fmt.Sprintf("%d-%s", row, col))) return El("td", Raw(strconv.Itoa(row)+"-"+col))
})) }))
}) })