(new): rich text
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package richtext
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func roundtripBlock(t *testing.T, in RichBlock) {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
out, err := UnmarshalBlock(b)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal %s: %v", b, err)
|
||||
}
|
||||
b2, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
t.Fatalf("remarshal: %v", err)
|
||||
}
|
||||
if string(b) != string(b2) {
|
||||
t.Fatalf("not stable:\n %s\n %s", b, b2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockRoundtrip(t *testing.T) {
|
||||
cases := []RichBlock{
|
||||
// wrap-блоки
|
||||
Paragraph(String("Hello, world")),
|
||||
SectionHeading(Bold(String("Chapter 1"))),
|
||||
Footer(String("© 2024")),
|
||||
Thinking(String("Let me reason step by step.")),
|
||||
|
||||
// preformatted
|
||||
BlockPreformatted{Text: String("fmt.Println(\"hi\")"), Language: "go"},
|
||||
BlockPreformatted{Text: String("no language")},
|
||||
|
||||
// цитаты
|
||||
BlockBlockQuotation{Text: String("To be or not to be"), Caption: String("Shakespeare")},
|
||||
BlockPullQuotation{Text: String("Pull me"), Caption: nil},
|
||||
|
||||
// список
|
||||
BlockList{
|
||||
Items: []RichBlockListItem{
|
||||
{Blocks: []RichBlock{Paragraph(String("item 1"))}},
|
||||
{Blocks: []RichBlock{Paragraph(String("item 2"))}},
|
||||
},
|
||||
Ordered: true,
|
||||
},
|
||||
BlockList{
|
||||
Items: []RichBlockListItem{
|
||||
{Blocks: []RichBlock{Paragraph(String("bullet"))}},
|
||||
},
|
||||
Ordered: false,
|
||||
},
|
||||
|
||||
// коллаж и слайдшоу
|
||||
BlockCollage{
|
||||
Items: []RichBlock{BlockPhoto{FileID: "abc123"}},
|
||||
Caption: String("A photo"),
|
||||
},
|
||||
BlockSlideshow{
|
||||
Items: []RichBlock{BlockVideo{FileID: "vid1", Autoplay: true, Loop: false}},
|
||||
Caption: nil,
|
||||
},
|
||||
|
||||
// details
|
||||
BlockDetails{
|
||||
Title: String("Spoiler"),
|
||||
Blocks: []RichBlock{Paragraph(String("Hidden content"))},
|
||||
Open: false,
|
||||
},
|
||||
BlockDetails{
|
||||
Title: Bold(String("Open details")),
|
||||
Blocks: []RichBlock{BlockDivider{}, Paragraph(String("content"))},
|
||||
Open: true,
|
||||
},
|
||||
|
||||
// таблица
|
||||
BlockTable{
|
||||
Title: String("Results"),
|
||||
Rows: [][]RichBlockTableCell{
|
||||
{
|
||||
{Content: []RichBlock{Paragraph(String("Cell A1"))}},
|
||||
{Content: []RichBlock{Paragraph(String("Cell A2"))}, ColumnSpan: 2},
|
||||
},
|
||||
{
|
||||
{Content: []RichBlock{Paragraph(String("Cell B1"))}, RowSpan: 2},
|
||||
{Content: []RichBlock{Paragraph(String("Cell B2"))}},
|
||||
},
|
||||
},
|
||||
Bordered: true,
|
||||
Striped: false,
|
||||
},
|
||||
|
||||
// карта
|
||||
BlockMap{
|
||||
Latitude: 55.7558, Longitude: 37.6173,
|
||||
Zoom: 12, Width: 800, Height: 400,
|
||||
Caption: String("Moscow"),
|
||||
},
|
||||
|
||||
// медиа
|
||||
BlockPhoto{FileID: "photo_file_id", Caption: String("A cat"), URL: "https://example.com/cat.jpg"},
|
||||
BlockPhoto{FileID: "bare_photo"},
|
||||
BlockVideo{FileID: "video_file_id", Caption: String("Demo"), Autoplay: true, Loop: true},
|
||||
BlockAudio{FileID: "audio_file_id", Caption: String("Podcast ep. 1")},
|
||||
BlockAnimation{FileID: "anim_file_id"},
|
||||
BlockVoiceNote{FileID: "voice_file_id"},
|
||||
|
||||
// листья
|
||||
BlockDivider{},
|
||||
BlockMathematicalExpression{Expression: "E = mc^2"},
|
||||
BlockAnchor{Name: "section-2"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
roundtripBlock(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRichMessageRoundtrip(t *testing.T) {
|
||||
msg := RichMessage{
|
||||
Blocks: []RichBlock{
|
||||
SectionHeading(String("Title")),
|
||||
Paragraph(Array{String("Some "), Bold(String("bold")), String(" text")}),
|
||||
BlockDivider{},
|
||||
BlockList{
|
||||
Items: []RichBlockListItem{
|
||||
{Blocks: []RichBlock{Paragraph(String("First"))}},
|
||||
{Blocks: []RichBlock{Paragraph(String("Second"))}},
|
||||
},
|
||||
Ordered: true,
|
||||
},
|
||||
BlockPhoto{FileID: "img1", Caption: String("Fig. 1")},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var out RichMessage
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
b2, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
t.Fatalf("remarshal: %v", err)
|
||||
}
|
||||
if string(b) != string(b2) {
|
||||
t.Fatalf("not stable:\n %s\n %s", b, b2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockDividerHasNoContent(t *testing.T) {
|
||||
b, _ := json.Marshal(BlockDivider{})
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(b, &m)
|
||||
if len(m) != 1 {
|
||||
t.Fatalf("divider must only have type field: %s", b)
|
||||
}
|
||||
if m["type"] != "divider" {
|
||||
t.Fatalf("unexpected type: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockUnknownTypeWithTextIsForwardCompat(t *testing.T) {
|
||||
raw := []byte(`{"type":"future_tag","text":"hello"}`)
|
||||
b, err := UnmarshalBlock(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("forward-compat failed: %v", err)
|
||||
}
|
||||
w, ok := b.(BlockWrap)
|
||||
if !ok || w.Tag != "future_tag" {
|
||||
t.Fatalf("expected BlockWrap{future_tag}, got %T", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockUnknownTypeWithoutTextIsError(t *testing.T) {
|
||||
raw := []byte(`{"type":"mystery_leaf","value":42}`)
|
||||
_, err := UnmarshalBlock(raw)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown type without text")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user