package render import ( "context" "strings" "testing" "git.dev.alexdunmow.com/block/core/blocks" "github.com/google/uuid" ) type testEmbedResolver struct{} func (testEmbedResolver) RenderEmbed(_ context.Context, blockID uuid.UUID, dataSource, layout string) string { return "embed:" + blockID.String() + ":" + dataSource + ":" + layout } func TestBlockNoteToHTMLUsesSDKContextResolvers(t *testing.T) { blockID := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := blocks.WithEmbedResolver(context.Background(), testEmbedResolver{}) ctx = blocks.WithHumanProofBanner(ctx, &blocks.HumanProofBannerData{ ActiveTimeMinutes: 12, KeystrokeCount: 3456, SessionCount: 2, PostSlug: "proof-post", }) html := BlockNoteToHTML(ctx, map[string]any{ "blocks": []any{ map[string]any{ "type": "embed", "props": map[string]any{ "blockId": blockID.String(), "dataSource": "row:22222222-2222-2222-2222-222222222222", "layout": "card", }, }, map[string]any{"type": "humanProof"}, }, }) if !strings.Contains(html, "embed:"+blockID.String()+":row:22222222-2222-2222-2222-222222222222:card") { t.Fatalf("BlockNoteToHTML() did not render embed from SDK context: %s", html) } if !strings.Contains(html, `data-human-proof-banner`) || !strings.Contains(html, `proof-post`) { t.Fatalf("BlockNoteToHTML() did not render human proof banner from SDK context: %s", html) } } func TestTextAlignment(t *testing.T) { tests := []struct { name string blockType string alignment string wantClass string }{ {"paragraph center", "paragraph", "center", "text-center"}, {"paragraph right", "paragraph", "right", "text-right"}, {"heading center", "heading", "center", "text-center"}, {"quote justify", "quote", "justify", "text-justify"}, {"paragraph left", "paragraph", "left", "text-left"}, {"paragraph default", "paragraph", "", "my-4"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { props := map[string]any{} if tt.alignment != "" { props["textAlignment"] = tt.alignment } if tt.blockType == "heading" { props["level"] = float64(2) } doc := map[string]any{ "blocks": []any{ map[string]any{ "type": tt.blockType, "props": props, "content": "Test", }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, tt.wantClass) { t.Errorf("expected class %q in output: %s", tt.wantClass, html) } }) } } func TestCheckListItem(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "checkListItem", "props": map[string]any{"checked": true}, "content": "Done task", }, map[string]any{ "type": "checkListItem", "props": map[string]any{"checked": false}, "content": "Todo task", }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, `checked`) { t.Errorf("expected checked attribute: %s", html) } if !strings.Contains(html, "Done task") || !strings.Contains(html, "Todo task") { t.Errorf("expected checklist content: %s", html) } if !strings.Contains(html, `type="checkbox"`) { t.Errorf("expected checkbox input: %s", html) } } func TestToggleListItem(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "toggleListItem", "content": "Details", "children": []any{ map[string]any{ "type": "paragraph", "content": "Nested content", }, }, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, "Bold text") { t.Errorf("expected bold formatting: %s", html) } } func TestColorValueToClass(t *testing.T) { tests := []struct { name string input string prefix string expected string }{ {"empty", "", "text", ""}, {"default", "default", "text", ""}, {"theme primary text", "primary", "text", "text-primary"}, {"theme primary bg", "primary", "bg", "bg-primary"}, {"theme foreground", "foreground", "text", "text-foreground"}, {"theme muted-foreground", "muted-foreground", "text", "text-muted-foreground"}, {"custom color text", "custom:brand", "text", "text-[hsl(var(--color-brand))]"}, {"custom color bg", "custom:brand", "bg", "bg-[hsl(var(--color-brand))]"}, {"hex color text", "hex:#ff5500", "text", "text-[#ff5500]"}, {"hex color bg", "hex:#ff5500", "bg", "bg-[#ff5500]"}, {"short hex", "hex:#f00", "text", "text-[#f00]"}, {"invalid hex length", "hex:#ff", "text", ""}, {"invalid hex char", "hex:#gggggg", "text", ""}, {"custom with invalid chars", "custom:bad name", "text", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := colorValueToClass(tt.input, tt.prefix) if got != tt.expected { t.Errorf("colorValueToClass(%q, %q) = %q, want %q", tt.input, tt.prefix, got, tt.expected) } }) } } func TestRenderInlineContentWithTextColor(t *testing.T) { content := []map[string]any{ { "type": "text", "text": "Hello", "styles": map[string]any{ "textColor": "primary", }, }, } html := renderInlineContent(content) if !strings.Contains(html, `class="text-primary"`) { t.Errorf("expected text color class: %s", html) } } func TestRenderInlineContentWithBackgroundColor(t *testing.T) { content := []map[string]any{ { "type": "text", "text": "Highlighted", "styles": map[string]any{ "backgroundColor": "hex:#ffcc00", }, }, } html := renderInlineContent(content) if !strings.Contains(html, `bg-[#ffcc00]`) { t.Errorf("expected background color class: %s", html) } } func TestRenderInlineContentWithBothColors(t *testing.T) { content := []map[string]any{ { "type": "text", "text": "Styled", "styles": map[string]any{ "textColor": "foreground", "backgroundColor": "custom:highlight", }, }, } html := renderInlineContent(content) if !strings.Contains(html, `text-foreground`) { t.Errorf("expected text color class: %s", html) } if !strings.Contains(html, `bg-[hsl(var(--color-highlight))]`) { t.Errorf("expected background color class: %s", html) } } func TestRenderInlineContentWithDefaultColor(t *testing.T) { content := []map[string]any{ { "type": "text", "text": "Normal", "styles": map[string]any{ "textColor": "default", }, }, } html := renderInlineContent(content) if strings.Contains(html, "class=") { t.Errorf("default color should not add class: %s", html) } } func TestRenderInlineContentWithColorsAndOtherStyles(t *testing.T) { content := []map[string]any{ { "type": "text", "text": "Bold and colored", "styles": map[string]any{ "bold": true, "textColor": "hex:#ff0000", }, }, } html := renderInlineContent(content) if !strings.Contains(html, "") { t.Errorf("expected bold tag: %s", html) } if !strings.Contains(html, `text-[#ff0000]`) { t.Errorf("expected text color class: %s", html) } } func TestBlockNoteToHTMLWithColoredParagraph(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "paragraph", "content": []any{ map[string]any{ "type": "text", "text": "Red text", "styles": map[string]any{ "textColor": "hex:#ff0000", }, }, }, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, "") { t.Errorf("expected thead: %s", html) } if strings.Contains(html, "") { t.Errorf("did not expect tbody for single-row table: %s", html) } } func TestTableCellStringContent(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "table", "content": map[string]any{ "rows": []any{ map[string]any{"cells": []any{"Header", "Value"}}, map[string]any{"cells": []any{"Row", "Cell"}}, }, }, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, "Header") || !strings.Contains(html, "Cell") { t.Errorf("expected table cell content: %s", html) } if !strings.Contains(html, "") { t.Errorf("expected tbody for multi-row table: %s", html) } } func TestNestedListRendering(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "bulletListItem", "content": "Parent", "children": []any{ map[string]any{ "type": "bulletListItem", "content": "Child", }, }, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, "Parent") || !strings.Contains(html, "Child") { t.Errorf("expected nested list content: %s", html) } if strings.Count(html, "") { t.Errorf("expected hard break in output: %s", html) } } func TestReferencesBlock(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "references", "props": map[string]any{ "items": `[{"text":"Steve Blank","url":"https://example.com/blank"},{"text":"Charity Majors","url":""}]`, }, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, `class="bn-references"`) { t.Errorf("expected bn-references class: %s", html) } if !strings.Contains(html, `Steve Blank`) { t.Errorf("expected linked reference: %s", html) } if !strings.Contains(html, "
  • Charity Majors") { t.Errorf("expected plain text reference: %s", html) } } func TestReferencesBlockEmpty(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "references", "props": map[string]any{"items": "[]"}, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, `class="bn-references"`) { t.Errorf("expected bn-references wrapper even when empty: %s", html) } } func TestReferencesBlockMalformedJSON(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "references", "props": map[string]any{"items": "not valid json"}, }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, `class="bn-references"`) { t.Errorf("expected graceful fallback: %s", html) } } func TestEmptyDocument(t *testing.T) { html := BlockNoteToHTML(context.Background(), map[string]any{}) if html != "" { t.Errorf("expected empty output for empty doc: %s", html) } } func TestUnknownBlockType(t *testing.T) { doc := map[string]any{ "blocks": []any{ map[string]any{ "type": "unknownBlock", "content": "Some content", }, }, } html := BlockNoteToHTML(context.Background(), doc) if !strings.Contains(html, "Some content") { t.Errorf("expected fallback rendering of unknown block: %s", html) } }