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>
557 lines
13 KiB
Go
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)
|
|
}
|