MediaValue gains an Attribution credit resolved through the existing MediaResolver context hook. The BlockNote image case renders width and align presets, alt text, an optional link wrap, and the attribution chip in chip, chipHover, and below modes with the same themed markup as the cms page-builder image block. Legacy url-and-caption blocks render byte-identical to the previous output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
783 lines
21 KiB
Go
783 lines
21 KiB
Go
package render
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.dev.alexdunmow.com/block/pluginsdk/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, "<details") {
|
|
t.Errorf("expected details element: %s", html)
|
|
}
|
|
if !strings.Contains(html, "<summary") {
|
|
t.Errorf("expected summary element: %s", html)
|
|
}
|
|
if !strings.Contains(html, "Nested content") {
|
|
t.Errorf("expected nested content: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestToggleListItemOpen(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "toggleListItem",
|
|
"props": map[string]any{"open": true},
|
|
"content": "Open toggle",
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
if !strings.Contains(html, ` open`) {
|
|
t.Errorf("expected open attribute: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestVideoBlock(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "video",
|
|
"props": map[string]any{
|
|
"url": "https://example.com/video.mp4",
|
|
"caption": "My video",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
|
|
if !strings.Contains(html, `<video`) {
|
|
t.Errorf("expected video element: %s", html)
|
|
}
|
|
if !strings.Contains(html, `controls`) {
|
|
t.Errorf("expected controls attribute: %s", html)
|
|
}
|
|
if !strings.Contains(html, "My video") {
|
|
t.Errorf("expected caption: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestAudioBlock(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "audio",
|
|
"props": map[string]any{
|
|
"url": "https://example.com/audio.mp3",
|
|
"caption": "Podcast episode",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
|
|
if !strings.Contains(html, `<audio`) {
|
|
t.Errorf("expected audio element: %s", html)
|
|
}
|
|
if !strings.Contains(html, `controls`) {
|
|
t.Errorf("expected controls attribute: %s", html)
|
|
}
|
|
if !strings.Contains(html, "Podcast episode") {
|
|
t.Errorf("expected caption: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestFileBlock(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "file",
|
|
"props": map[string]any{
|
|
"url": "https://example.com/doc.pdf",
|
|
"name": "Document.pdf",
|
|
"caption": "Download here",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
|
|
if !strings.Contains(html, `href="https://example.com/doc.pdf"`) {
|
|
t.Errorf("expected file link: %s", html)
|
|
}
|
|
if !strings.Contains(html, "Document.pdf") {
|
|
t.Errorf("expected file name: %s", html)
|
|
}
|
|
if !strings.Contains(html, "Download here") {
|
|
t.Errorf("expected caption: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestFileBlockNoName(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "file",
|
|
"props": map[string]any{
|
|
"url": "https://example.com/doc.pdf",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
if !strings.Contains(html, "https://example.com/doc.pdf") {
|
|
t.Errorf("expected URL as fallback name: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestStatementBlock(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "statement",
|
|
"content": []any{
|
|
map[string]any{
|
|
"type": "text",
|
|
"text": "This is a key statement.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
|
|
if !strings.Contains(html, `class="bn-statement"`) {
|
|
t.Errorf("expected bn-statement class: %s", html)
|
|
}
|
|
if !strings.Contains(html, "This is a key statement.") {
|
|
t.Errorf("expected statement text: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestStatementBlockWithFormatting(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "statement",
|
|
"content": []any{
|
|
map[string]any{
|
|
"type": "text",
|
|
"text": "Bold text",
|
|
"styles": map[string]any{"bold": true},
|
|
},
|
|
map[string]any{
|
|
"type": "text",
|
|
"text": " and normal.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
|
|
if !strings.Contains(html, "<strong>Bold text</strong>") {
|
|
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, false)
|
|
|
|
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, false)
|
|
|
|
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, false)
|
|
|
|
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, false)
|
|
|
|
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, false)
|
|
|
|
if !strings.Contains(html, "<strong>") {
|
|
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, "<p") {
|
|
t.Errorf("expected paragraph tag: %s", html)
|
|
}
|
|
if !strings.Contains(html, `text-[#ff0000]`) {
|
|
t.Errorf("expected text color class: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestBulletListStringContent(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "bulletListItem",
|
|
"content": "Test bullet content",
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
if !strings.Contains(html, "Test bullet content") {
|
|
t.Errorf("expected bullet content: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestTableSingleRowDoesNotEmitTbody(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{
|
|
[]any{map[string]any{"type": "text", "text": "Header"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
if !strings.Contains(html, "<thead>") {
|
|
t.Errorf("expected thead: %s", html)
|
|
}
|
|
if strings.Contains(html, "<tbody>") {
|
|
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, "<tbody>") {
|
|
t.Errorf("expected tbody for multi-row table: %s", html)
|
|
}
|
|
}
|
|
|
|
// BlockNote >=0.15 (the editor ships 0.47) stores each table cell as a
|
|
// tableCell object: {type:"tableCell", props:{...}, content:[...inline]}.
|
|
// The renderer must unwrap content[] from these objects, not just bare
|
|
// inline arrays/strings.
|
|
func TestTableCellObjectContent(t *testing.T) {
|
|
cell := func(text string) map[string]any {
|
|
return map[string]any{
|
|
"type": "tableCell",
|
|
"props": map[string]any{},
|
|
"content": []any{map[string]any{"type": "text", "text": text}},
|
|
}
|
|
}
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "table",
|
|
"content": map[string]any{
|
|
"type": "tableContent",
|
|
"rows": []any{
|
|
map[string]any{"cells": []any{cell("Header A"), cell("Header B")}},
|
|
map[string]any{"cells": []any{cell("Cell A"), cell("Cell B")}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
for _, want := range []string{"Header A", "Header B", "Cell A", "Cell B"} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("expected table cell content %q in output: %s", want, 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, "<ul") < 2 {
|
|
t.Errorf("expected nested ul elements: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestHardBreakInlineContent(t *testing.T) {
|
|
doc := map[string]any{
|
|
"blocks": []any{
|
|
map[string]any{
|
|
"type": "paragraph",
|
|
"content": []any{
|
|
map[string]any{"type": "text", "text": "Line1"},
|
|
map[string]any{"type": "hardBreak"},
|
|
map[string]any{"type": "text", "text": "Line2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
html := BlockNoteToHTML(context.Background(), doc)
|
|
if !strings.Contains(html, "Line1") || !strings.Contains(html, "Line2") || !strings.Contains(html, "<br />") {
|
|
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, `<a href="https://example.com/blank">Steve Blank`) {
|
|
t.Errorf("expected linked reference: %s", html)
|
|
}
|
|
if !strings.Contains(html, "<li>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)
|
|
}
|
|
}
|
|
|
|
type testMediaResolver struct {
|
|
att *blocks.MediaAttribution
|
|
}
|
|
|
|
func (r testMediaResolver) ResolveMedia(_ context.Context, mediaID uuid.UUID) *blocks.MediaValue {
|
|
return &blocks.MediaValue{Src: "/media/" + mediaID.String(), Attribution: r.att}
|
|
}
|
|
|
|
func imageBlock(props map[string]any) map[string]any {
|
|
return map[string]any{"blocks": []any{map[string]any{"type": "image", "props": props}}}
|
|
}
|
|
|
|
// Legacy blocks (url/caption only) must render byte-identical to the old output.
|
|
func TestImageBlockLegacyUnchanged(t *testing.T) {
|
|
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
|
"url": "https://example.com/pic.jpg", "caption": "A pic",
|
|
}))
|
|
want := "<figure class=\"my-6\"><img src=\"https://example.com/pic.jpg\" alt=\"A pic\" /><figcaption>A pic</figcaption></figure>\n"
|
|
if html != want {
|
|
t.Fatalf("legacy image output changed:\n got: %s\nwant: %s", html, want)
|
|
}
|
|
}
|
|
|
|
func TestImageBlockAltAndLink(t *testing.T) {
|
|
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
|
"url": "/media/x.webp", "alt": "Sunset", "link": "https://example.com/story",
|
|
}))
|
|
if !strings.Contains(html, `alt="Sunset"`) {
|
|
t.Fatalf("alt not rendered: %s", html)
|
|
}
|
|
if !strings.Contains(html, `<a href="https://example.com/story"><img `) {
|
|
t.Fatalf("link wrap not rendered: %s", html)
|
|
}
|
|
}
|
|
|
|
func TestImageBlockWidthAndAlign(t *testing.T) {
|
|
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
|
"url": "/media/x.webp", "width": "medium", "align": "right",
|
|
}))
|
|
if !strings.Contains(html, `max-w-[600px]`) || !strings.Contains(html, `float-right ml-6 mb-4`) {
|
|
t.Fatalf("width/align classes missing: %s", html)
|
|
}
|
|
|
|
centered := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
|
"url": "/media/x.webp", "width": "small", "align": "center",
|
|
}))
|
|
if !strings.Contains(centered, `max-w-[400px]`) || !strings.Contains(centered, `mx-auto`) {
|
|
t.Fatalf("centered width classes missing: %s", centered)
|
|
}
|
|
}
|
|
|
|
func TestImageBlockAttributionModes(t *testing.T) {
|
|
mediaID := uuid.MustParse("33333333-3333-3333-3333-333333333333")
|
|
att := &blocks.MediaAttribution{
|
|
Photographer: "Jane Doe",
|
|
PhotographerURL: "https://www.pexels.com/@janedoe",
|
|
Source: "pexels",
|
|
SourceURL: "https://www.pexels.com/photo/123",
|
|
}
|
|
ctx := blocks.WithMediaResolver(context.Background(), testMediaResolver{att: att})
|
|
props := func(mode string) map[string]any {
|
|
return map[string]any{"url": "/media/x.webp", "mediaId": mediaID.String(), "attributionMode": mode}
|
|
}
|
|
|
|
chip := BlockNoteToHTML(ctx, imageBlock(props("chip")))
|
|
if !strings.Contains(chip, "Photo by ") || !strings.Contains(chip, "Jane Doe") ||
|
|
!strings.Contains(chip, "on ") || !strings.Contains(chip, "Pexels") ||
|
|
!strings.Contains(chip, `absolute bottom-2 right-2`) {
|
|
t.Fatalf("chip mode wrong: %s", chip)
|
|
}
|
|
|
|
hover := BlockNoteToHTML(ctx, imageBlock(props("chipHover")))
|
|
if !strings.Contains(hover, `group-hover:opacity-100`) || !strings.Contains(hover, `class="group relative overflow-hidden"`) {
|
|
t.Fatalf("chipHover mode wrong: %s", hover)
|
|
}
|
|
|
|
below := BlockNoteToHTML(ctx, imageBlock(props("below")))
|
|
if !strings.Contains(below, `mt-1 text-xs text-muted-foreground text-center`) || !strings.Contains(below, "Jane Doe") {
|
|
t.Fatalf("below mode wrong: %s", below)
|
|
}
|
|
|
|
off := BlockNoteToHTML(ctx, imageBlock(map[string]any{"url": "/media/x.webp", "mediaId": mediaID.String()}))
|
|
if strings.Contains(off, "Photo by") {
|
|
t.Fatalf("default mode must render no credit: %s", off)
|
|
}
|
|
}
|
|
|
|
func TestImageBlockAttributionDegradesGracefully(t *testing.T) {
|
|
mediaID := uuid.MustParse("44444444-4444-4444-4444-444444444444")
|
|
props := imageBlock(map[string]any{"url": "/media/x.webp", "mediaId": mediaID.String(), "attributionMode": "chip"})
|
|
|
|
// No resolver in ctx.
|
|
if html := BlockNoteToHTML(context.Background(), props); strings.Contains(html, "Photo by") {
|
|
t.Fatalf("no-resolver ctx must render no credit: %s", html)
|
|
}
|
|
// Resolver returns media without attribution.
|
|
ctx := blocks.WithMediaResolver(context.Background(), testMediaResolver{att: nil})
|
|
if html := BlockNoteToHTML(ctx, props); strings.Contains(html, "Photo by") {
|
|
t.Fatalf("nil attribution must render no credit: %s", html)
|
|
}
|
|
// Photographer-only credit omits the "on SOURCE" suffix.
|
|
ctx = blocks.WithMediaResolver(context.Background(), testMediaResolver{att: &blocks.MediaAttribution{Photographer: "Jane"}})
|
|
html := BlockNoteToHTML(ctx, props)
|
|
if !strings.Contains(html, "Photo by ") || strings.Contains(html, " on ") {
|
|
t.Fatalf("photographer-only credit wrong: %s", html)
|
|
}
|
|
}
|