Files
probo/pkg/prosemirror/node_test.go
Sacha Al Himdani c7e2fc209f Use probo.com for bare and marketing domain references
Replace the bare `getprobo.com` domain and the `www.getprobo.com`
marketing host with `probo.com` / `www.probo.com` across the codebase.
Functional subdomains (app, console, notification, custom, test,
cookie-banner, compliance) keep their existing `getprobo.com` hosts,
and changelog entries are left untouched.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-11 13:46:10 +02:00

557 lines
13 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package prosemirror
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadTestDocument(t *testing.T) Node {
t.Helper()
data, err := os.ReadFile("testdata/document.json")
require.NoError(t, err)
var doc Node
require.NoError(t, json.Unmarshal(data, &doc))
return doc
}
func TestUnmarshalDocument(t *testing.T) {
t.Parallel()
doc := loadTestDocument(t)
assert.Equal(t, NodeDoc, doc.Type)
require.Len(t, doc.Content, 14)
t.Run(
"heading level 1",
func(t *testing.T) {
t.Parallel()
h1 := doc.Content[0]
assert.Equal(t, NodeHeading, h1.Type)
attrs, err := h1.HeadingAttrs()
require.NoError(t, err)
assert.Equal(t, 1, attrs.Level)
require.Len(t, h1.Content, 1)
assert.Equal(t, NodeText, h1.Content[0].Type)
require.NotNil(t, h1.Content[0].Text)
assert.Equal(t, "Heading 1", *h1.Content[0].Text)
},
)
t.Run(
"paragraph with mixed marks",
func(t *testing.T) {
t.Parallel()
p := doc.Content[1]
assert.Equal(t, NodeParagraph, p.Type)
require.True(t, len(p.Content) > 5)
// Bold text
boldNode := p.Content[1]
require.Len(t, boldNode.Marks, 1)
assert.Equal(t, MarkStrong, boldNode.Marks[0].Type)
require.NotNil(t, boldNode.Text)
assert.Equal(t, "with some bold", *boldNode.Text)
// Italic text
italicNode := p.Content[3]
require.Len(t, italicNode.Marks, 1)
assert.Equal(t, MarkEm, italicNode.Marks[0].Type)
// Underline text
underlineNode := p.Content[5]
require.Len(t, underlineNode.Marks, 1)
assert.Equal(t, MarkUnderline, underlineNode.Marks[0].Type)
// Hard break
assert.Equal(t, NodeHardBreak, p.Content[7].Type)
// Strikethrough
strikeNode := p.Content[9]
require.Len(t, strikeNode.Marks, 1)
assert.Equal(t, MarkStrike, strikeNode.Marks[0].Type)
// Inline code
codeNode := p.Content[12]
require.Len(t, codeNode.Marks, 1)
assert.Equal(t, MarkCode, codeNode.Marks[0].Type)
// Link
linkNode := p.Content[14]
require.Len(t, linkNode.Marks, 1)
assert.Equal(t, MarkLink, linkNode.Marks[0].Type)
linkAttrs, err := linkNode.Marks[0].LinkAttrs()
require.NoError(t, err)
assert.Equal(t, "https://probo.com", linkAttrs.Href)
require.NotNil(t, linkAttrs.Target)
assert.Equal(t, "_blank", *linkAttrs.Target)
require.NotNil(t, linkAttrs.Rel)
assert.Equal(t, "noopener noreferrer nofollow", *linkAttrs.Rel)
assert.Nil(t, linkAttrs.Class)
assert.Nil(t, linkAttrs.Title)
},
)
t.Run(
"heading level 2",
func(t *testing.T) {
t.Parallel()
h2 := doc.Content[2]
assert.Equal(t, NodeHeading, h2.Type)
attrs, err := h2.HeadingAttrs()
require.NoError(t, err)
assert.Equal(t, 2, attrs.Level)
},
)
t.Run(
"code block",
func(t *testing.T) {
t.Parallel()
cb := doc.Content[4]
assert.Equal(t, NodeCodeBlock, cb.Type)
attrs, err := cb.CodeBlockAttrs()
require.NoError(t, err)
assert.Nil(t, attrs.Language)
require.Len(t, cb.Content, 1)
require.NotNil(t, cb.Content[0].Text)
assert.Equal(t, "code block", *cb.Content[0].Text)
},
)
t.Run(
"heading level 3",
func(t *testing.T) {
t.Parallel()
h3 := doc.Content[5]
attrs, err := h3.HeadingAttrs()
require.NoError(t, err)
assert.Equal(t, 3, attrs.Level)
},
)
t.Run(
"bullet list",
func(t *testing.T) {
t.Parallel()
bl := doc.Content[6]
assert.Equal(t, NodeBulletList, bl.Type)
require.Len(t, bl.Content, 3)
for _, item := range bl.Content {
assert.Equal(t, NodeListItem, item.Type)
require.Len(t, item.Content, 1)
assert.Equal(t, NodeParagraph, item.Content[0].Type)
}
},
)
t.Run(
"horizontal rule",
func(t *testing.T) {
t.Parallel()
assert.Equal(t, NodeHorizontalRule, doc.Content[7].Type)
assert.Equal(t, NodeHorizontalRule, doc.Content[9].Type)
assert.Equal(t, NodeHorizontalRule, doc.Content[11].Type)
},
)
t.Run(
"ordered list",
func(t *testing.T) {
t.Parallel()
ol := doc.Content[8]
assert.Equal(t, NodeOrderedList, ol.Type)
require.Len(t, ol.Content, 3)
attrs, err := ol.OrderedListAttrs()
require.NoError(t, err)
assert.Equal(t, 1, attrs.Start)
assert.Nil(t, attrs.Type)
},
)
t.Run(
"blockquote",
func(t *testing.T) {
t.Parallel()
bq := doc.Content[10]
assert.Equal(t, NodeBlockquote, bq.Type)
require.Len(t, bq.Content, 1)
assert.Equal(t, NodeParagraph, bq.Content[0].Type)
// Verify hard break inside blockquote
bqPara := bq.Content[0]
require.True(t, len(bqPara.Content) >= 3)
assert.Equal(t, NodeHardBreak, bqPara.Content[1].Type)
},
)
t.Run(
"table",
func(t *testing.T) {
t.Parallel()
table := doc.Content[12]
assert.Equal(t, NodeTable, table.Type)
require.Len(t, table.Content, 3)
// Header row
headerRow := table.Content[0]
assert.Equal(t, NodeTableRow, headerRow.Type)
require.Len(t, headerRow.Content, 4)
for _, cell := range headerRow.Content {
assert.Equal(t, NodeTableHeader, cell.Type)
}
// Last header has colwidth
th4 := headerRow.Content[3]
thAttrs, err := th4.TableCellAttrs()
require.NoError(t, err)
assert.Equal(t, 1, thAttrs.Colspan)
assert.Equal(t, 1, thAttrs.Rowspan)
assert.Equal(t, []int{61}, thAttrs.Colwidth)
// First header has null colwidth
th1 := headerRow.Content[0]
th1Attrs, err := th1.TableCellAttrs()
require.NoError(t, err)
assert.Nil(t, th1Attrs.Colwidth)
// Data rows
for _, row := range table.Content[1:] {
assert.Equal(t, NodeTableRow, row.Type)
require.Len(t, row.Content, 4)
for _, cell := range row.Content {
assert.Equal(t, NodeTableCell, cell.Type)
}
}
// Nested bullet list in table cell
nestedBL := table.Content[1].Content[3]
require.Len(t, nestedBL.Content, 1)
assert.Equal(t, NodeBulletList, nestedBL.Content[0].Type)
// Nested ordered list in table cell
nestedOL := table.Content[2].Content[3]
require.Len(t, nestedOL.Content, 1)
assert.Equal(t, NodeOrderedList, nestedOL.Content[0].Type)
},
)
t.Run(
"trailing empty paragraph",
func(t *testing.T) {
t.Parallel()
emptyP := doc.Content[13]
assert.Equal(t, NodeParagraph, emptyP.Type)
assert.Empty(t, emptyP.Content)
},
)
}
func TestMarshalRoundtrip(t *testing.T) {
t.Parallel()
doc := loadTestDocument(t)
marshaled, err := json.Marshal(doc)
require.NoError(t, err)
var roundtripped Node
require.NoError(t, json.Unmarshal(marshaled, &roundtripped))
// Re-marshal both to compact JSON for comparison, since the original
// testdata file is pretty-printed and RawMessage preserves whitespace.
expected, err := json.Marshal(roundtripped)
require.NoError(t, err)
assert.JSONEq(t, string(marshaled), string(expected))
}
func TestHeadingAttrs(t *testing.T) {
t.Parallel()
raw := `{"type":"heading","attrs":{"level":3},"content":[{"type":"text","text":"Hello"}]}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.HeadingAttrs()
require.NoError(t, err)
assert.Equal(t, 3, attrs.Level)
}
func TestCodeBlockAttrs(t *testing.T) {
t.Parallel()
t.Run(
"with language",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.CodeBlockAttrs()
require.NoError(t, err)
require.NotNil(t, attrs.Language)
assert.Equal(t, "go", *attrs.Language)
},
)
t.Run(
"with null language",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"codeBlock","attrs":{"language":null}}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.CodeBlockAttrs()
require.NoError(t, err)
assert.Nil(t, attrs.Language)
},
)
}
func TestOrderedListAttrs(t *testing.T) {
t.Parallel()
raw := `{"type":"orderedList","attrs":{"start":5,"type":null}}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.OrderedListAttrs()
require.NoError(t, err)
assert.Equal(t, 5, attrs.Start)
assert.Nil(t, attrs.Type)
}
func TestImageAttrs(t *testing.T) {
t.Parallel()
raw := `{"type":"image","attrs":{"src":"https://example.com/img.png","alt":"An image","title":null}}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.ImageAttrs()
require.NoError(t, err)
assert.Equal(t, "https://example.com/img.png", attrs.Src)
require.NotNil(t, attrs.Alt)
assert.Equal(t, "An image", *attrs.Alt)
assert.Nil(t, attrs.Title)
}
func TestTableCellAttrs(t *testing.T) {
t.Parallel()
t.Run(
"with colwidth",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"tableCell","attrs":{"colspan":2,"rowspan":1,"colwidth":[100,200]}}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.TableCellAttrs()
require.NoError(t, err)
assert.Equal(t, 2, attrs.Colspan)
assert.Equal(t, 1, attrs.Rowspan)
assert.Equal(t, []int{100, 200}, attrs.Colwidth)
},
)
t.Run(
"with null colwidth",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null}}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
attrs, err := n.TableCellAttrs()
require.NoError(t, err)
assert.Equal(t, 1, attrs.Colspan)
assert.Equal(t, 1, attrs.Rowspan)
assert.Nil(t, attrs.Colwidth)
},
)
}
func TestLinkAttrs(t *testing.T) {
t.Parallel()
raw := `{"type":"link","attrs":{"href":"https://example.com","target":"_blank","rel":"noopener","class":null,"title":"Example"}}`
var m Mark
require.NoError(t, json.Unmarshal([]byte(raw), &m))
attrs, err := m.LinkAttrs()
require.NoError(t, err)
assert.Equal(t, "https://example.com", attrs.Href)
require.NotNil(t, attrs.Target)
assert.Equal(t, "_blank", *attrs.Target)
require.NotNil(t, attrs.Rel)
assert.Equal(t, "noopener", *attrs.Rel)
assert.Nil(t, attrs.Class)
require.NotNil(t, attrs.Title)
assert.Equal(t, "Example", *attrs.Title)
}
func TestTextLength(t *testing.T) {
t.Parallel()
t.Run(
"empty doc",
func(t *testing.T) {
t.Parallel()
n := Node{Type: NodeDoc}
assert.Equal(t, 0, n.TextLength())
},
)
t.Run(
"single text node",
func(t *testing.T) {
t.Parallel()
text := "hello"
n := Node{Type: NodeText, Text: &text}
assert.Equal(t, 5, n.TextLength())
},
)
t.Run(
"paragraph with text",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}`
doc, err := Parse(raw)
require.NoError(t, err)
assert.Equal(t, 11, doc.TextLength())
},
)
t.Run(
"multiple paragraphs",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"aaa"}]},{"type":"paragraph","content":[{"type":"text","text":"bb"}]}]}`
doc, err := Parse(raw)
require.NoError(t, err)
assert.Equal(t, 5, doc.TextLength())
},
)
t.Run(
"formatted text counts only text",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"plain "},{"type":"text","marks":[{"type":"bold"}],"text":"bold"}]}]}`
doc, err := Parse(raw)
require.NoError(t, err)
assert.Equal(t, 10, doc.TextLength())
},
)
t.Run(
"nested list structure",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"doc","content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item 1"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"item 2"}]}]}]}]}`
doc, err := Parse(raw)
require.NoError(t, err)
assert.Equal(t, 12, doc.TextLength())
},
)
t.Run(
"multi-byte unicode characters",
func(t *testing.T) {
t.Parallel()
raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"café résumé"}]}]}`
doc, err := Parse(raw)
require.NoError(t, err)
assert.Equal(t, 11, doc.TextLength())
},
)
t.Run(
"testdata document",
func(t *testing.T) {
t.Parallel()
doc := loadTestDocument(t)
assert.Greater(t, doc.TextLength(), 0)
},
)
}
func TestNodeWithNoAttrs(t *testing.T) {
t.Parallel()
raw := `{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}`
var n Node
require.NoError(t, json.Unmarshal([]byte(raw), &n))
assert.Equal(t, NodeParagraph, n.Type)
assert.Nil(t, n.Attrs)
require.Len(t, n.Content, 1)
require.NotNil(t, n.Content[0].Text)
assert.Equal(t, "Hello", *n.Content[0].Text)
}