initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
251
internal/tokenengine/tokens_test.go
Normal file
251
internal/tokenengine/tokens_test.go
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
package tokenengine
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urpi/go-bananas/internal/domain"
|
||||
)
|
||||
|
||||
// testTheme builds a minimal but complete theme for engine tests.
|
||||
func testTheme() *domain.Theme {
|
||||
return &domain.Theme{
|
||||
Slug: "acme",
|
||||
Name: "Acme",
|
||||
Status: domain.StatusPublished,
|
||||
DefaultAppearance: domain.AppearanceLight,
|
||||
Version: 3,
|
||||
Tokens: domain.TokenDoc{
|
||||
"color": map[string]any{
|
||||
"palette": map[string]any{
|
||||
"brand": map[string]any{
|
||||
"500": map[string]any{TypeKey: string(KindColor), ValueKey: "#4f46e5"},
|
||||
"600": map[string]any{TypeKey: string(KindColor), ValueKey: "#4338ca"},
|
||||
},
|
||||
},
|
||||
"semantic": map[string]any{
|
||||
"background": map[string]any{TypeKey: string(KindColor), ValueKey: "#ffffff"},
|
||||
"surface": map[string]any{TypeKey: string(KindColor), ValueKey: "#f8fafc"},
|
||||
"text": map[string]any{TypeKey: string(KindColor), ValueKey: "#0f172a"},
|
||||
"primary": map[string]any{TypeKey: string(KindColor), ValueKey: "#4f46e5"},
|
||||
},
|
||||
},
|
||||
"font": map[string]any{
|
||||
"family": map[string]any{
|
||||
"sans": map[string]any{TypeKey: string(KindFamilyAlias()), ValueKey: "Inter, sans-serif"},
|
||||
},
|
||||
},
|
||||
"space": map[string]any{
|
||||
"4": map[string]any{TypeKey: string(KindDimension), ValueKey: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// KindFamilyAlias exists only so the fixture reads clearly; it resolves to the
|
||||
// canonical font-family kind.
|
||||
func KindFamilyAlias() Kind { return KindFontFamily }
|
||||
|
||||
func TestResolveFlattensNestedTokens(t *testing.T) {
|
||||
doc := Resolve(testTheme(), domain.AppearanceLight)
|
||||
|
||||
if got, ok := doc.Lookup("color.semantic.background"); !ok {
|
||||
t.Fatalf("expected color.semantic.background to resolve, got %d tokens", len(doc.Tokens))
|
||||
} else if got.CSS != "#ffffff" {
|
||||
t.Errorf("background = %q, want #ffffff", got.CSS)
|
||||
}
|
||||
|
||||
space, ok := doc.Lookup("space.4")
|
||||
if !ok {
|
||||
t.Fatal("expected space.4 to resolve")
|
||||
}
|
||||
// A bare number in a dimension group must pick up the px suffix.
|
||||
if space.CSS != "1px" {
|
||||
t.Errorf("space.4 = %q, want 1px", space.CSS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveIsDeterministic(t *testing.T) {
|
||||
first := Resolve(testTheme(), domain.AppearanceLight)
|
||||
second := Resolve(testTheme(), domain.AppearanceLight)
|
||||
|
||||
if len(first.Tokens) != len(second.Tokens) {
|
||||
t.Fatalf("token count differs: %d vs %d", len(first.Tokens), len(second.Tokens))
|
||||
}
|
||||
for i := range first.Tokens {
|
||||
if first.Tokens[i].Path != second.Tokens[i].Path {
|
||||
t.Fatalf("token %d out of order: %q vs %q", i, first.Tokens[i].Path, second.Tokens[i].Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppearanceOverridesWinOverBase(t *testing.T) {
|
||||
theme := testTheme()
|
||||
theme.Tokens[AppearancesKey] = map[string]any{
|
||||
"dark": map[string]any{
|
||||
"color": map[string]any{
|
||||
"semantic": map[string]any{
|
||||
"background": map[string]any{TypeKey: string(KindColor), ValueKey: "#0b1220"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
light := Resolve(theme, domain.AppearanceLight)
|
||||
dark := Resolve(theme, domain.AppearanceDark)
|
||||
|
||||
if got, _ := light.Lookup("color.semantic.background"); got.CSS != "#ffffff" {
|
||||
t.Errorf("light background = %q, want the base value #ffffff", got.CSS)
|
||||
}
|
||||
if got, _ := dark.Lookup("color.semantic.background"); got.CSS != "#0b1220" {
|
||||
t.Errorf("dark background = %q, want the override #0b1220", got.CSS)
|
||||
}
|
||||
// Tokens not overridden must fall through to the base value.
|
||||
if got, _ := dark.Lookup("color.semantic.primary"); got.CSS != "#4f46e5" {
|
||||
t.Errorf("dark primary = %q, want the base value #4f46e5", got.CSS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderEmitsScopedSelectorsAndDerivedTokens(t *testing.T) {
|
||||
css := Render(testTheme(), RenderOptions{
|
||||
Slug: "acme",
|
||||
Appearance: domain.AppearanceLight,
|
||||
Derive: DefaultDerive(),
|
||||
Components: true,
|
||||
})
|
||||
|
||||
for _, want := range []string{
|
||||
`[data-bananas-theme="acme"]`,
|
||||
`[data-bananas-theme="acme"][data-bananas-appearance=dark]`,
|
||||
"--bn-color-semantic-background: #ffffff;",
|
||||
// Derived foreground must pick white on a dark primary.
|
||||
"--bn-color-on-semantic-primary: #ffffff;",
|
||||
"--bn-color-semantic-primary-hover:",
|
||||
"--bn-space-4: 1px;",
|
||||
"@layer bananas",
|
||||
".bn-btn",
|
||||
`.dark [data-bananas-theme="acme"]`,
|
||||
} {
|
||||
if !strings.Contains(css, want) {
|
||||
t.Errorf("rendered CSS missing %q\n---\n%s", want, css)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMinifiedOmitsCommentsAndWhitespace(t *testing.T) {
|
||||
css := Render(testTheme(), RenderOptions{Slug: "acme", Minify: true, Derive: DefaultDerive()})
|
||||
|
||||
if strings.Contains(css, "/*") {
|
||||
t.Error("minified CSS must not contain comments")
|
||||
}
|
||||
if strings.Contains(css, "\n\n") {
|
||||
t.Error("minified CSS must not contain blank lines")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAcceptsACompleteTheme(t *testing.T) {
|
||||
result := Validate(testTheme().Tokens)
|
||||
for _, issue := range result.Errors() {
|
||||
// The fixture is intentionally sparse; it must only trip the "unknown
|
||||
// group" rule if the schema drifted, never on the tokens it defines.
|
||||
if strings.Contains(issue.Message, "unknown token group") {
|
||||
t.Errorf("unexpected schema error: %+v", issue)
|
||||
}
|
||||
}
|
||||
if result.TokenCount != 8 {
|
||||
t.Errorf("TokenCount = %d, want 8", result.TokenCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsMalformedColour(t *testing.T) {
|
||||
doc := testTheme().Tokens
|
||||
doc["color"].(map[string]any)["semantic"].(map[string]any)["primary"] = map[string]any{
|
||||
TypeKey: string(KindColor), ValueKey: "#nothex",
|
||||
}
|
||||
|
||||
result := Validate(doc)
|
||||
if result.Valid {
|
||||
t.Fatal("expected validation to fail on a malformed colour")
|
||||
}
|
||||
found := false
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == "color.semantic.primary" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected an issue on color.semantic.primary, got %+v", result.Issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsUnknownGroup(t *testing.T) {
|
||||
doc := testTheme().Tokens
|
||||
doc["colour"] = map[string]any{"typo": map[string]any{ValueKey: "#fff"}}
|
||||
|
||||
result := Validate(doc)
|
||||
if result.Valid {
|
||||
t.Fatal("expected validation to reject an unknown top-level group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariableNameNormalisation(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"color.semantic.background": "--bn-color-semantic-background",
|
||||
"space.4": "--bn-space-4",
|
||||
"font.lineHeight.tight": "--bn-font-line-height-tight",
|
||||
"font.line height": "--bn-font-line-height",
|
||||
"weird.Name_Here": "--bn-weird-name-here",
|
||||
}
|
||||
for path, want := range cases {
|
||||
if got := VariableName(path); got != want {
|
||||
t.Errorf("VariableName(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndDeleteToken(t *testing.T) {
|
||||
doc := testTheme().Tokens
|
||||
|
||||
if err := SetIn(doc, "space.6", 1.5, KindDimension, ""); err != nil {
|
||||
t.Fatalf("SetIn: %v", err)
|
||||
}
|
||||
if got, err := LookupIn(doc, "space.6"); err != nil {
|
||||
t.Fatalf("LookupIn: %v", err)
|
||||
} else if got[ValueKey] != 1.5 {
|
||||
t.Errorf("space.6 = %v, want 1.5", got[ValueKey])
|
||||
}
|
||||
|
||||
deleted, err := DeleteIn(doc, "space.6")
|
||||
if err != nil || !deleted {
|
||||
t.Fatalf("DeleteIn = %v, %v; want true, nil", deleted, err)
|
||||
}
|
||||
if _, err := LookupIn(doc, "space.6"); err == nil {
|
||||
t.Error("token should be gone after DeleteIn")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetInRejectsUnknownGroup(t *testing.T) {
|
||||
doc := domain.TokenDoc{}
|
||||
err := SetIn(doc, "colour.typo", "#fff", KindColor, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected SetIn to reject an unknown group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPayloadCarriesFlatAndNestedForms(t *testing.T) {
|
||||
payload := JSONPayload(testTheme(), domain.AppearanceDark)
|
||||
|
||||
if payload.Theme.Slug != "acme" {
|
||||
t.Errorf("slug = %q, want acme", payload.Theme.Slug)
|
||||
}
|
||||
if payload.Variable["--bn-color-semantic-primary"] != "#4f46e5" {
|
||||
t.Errorf("flat map missing primary: %v", payload.Variable)
|
||||
}
|
||||
semantic, ok := payload.Tokens["color"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nested tokens missing the color group")
|
||||
}
|
||||
if _, ok := semantic["semantic"]; !ok {
|
||||
t.Error("nested tokens missing the semantic group")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue