| @@ -2,50 +2,145 @@ package devianter |
| 2 | |
2 | |
| 3 | import "testing" |
3 | import "testing" |
| 4 | |
4 | |
| 5 | // Regression: flattenComment's shape check used to read m[0] and m[len(m)-1] |
5 | // Regression: the shape check used to read m[0] and m[len(m)-1] without a length |
| 6 | // without a length check, so a comment with an empty markup body panicked with |
6 | // check, so a comment with an empty markup body panicked with index out of range |
| 7 | // index out of range and killed the caller's process. |
7 | // and killed the caller's process. |
| 8 | func TestFlattenCommentEmptyMarkup(t *testing.T) { |
8 | func TestFlattenMarkupEmptyMarkup(t *testing.T) { |
| 9 | if got := flattenComment(""); got != "" { |
9 | if got := flattenMarkup(""); got != "" { |
| 10 | t.Errorf("want an empty comment for empty markup, got %q", got) |
10 | t.Errorf("want an empty comment for empty markup, got %q", got) |
| 11 | } |
11 | } |
| 12 | } |
12 | } |
| 13 | |
13 | |
| 14 | func TestFlattenComment(t *testing.T) { |
14 | // tiptap is what DeviantArt actually serves today; the fixtures below are shaped |
| 15 | // A newer, Draft.js-encoded body is flattened to its text. |
15 | // like responses captured from the live API. |
| 16 | draft := `{"blocks":[{"text":"hello there"}]}` |
16 | func TestFlattenMarkupTiptap(t *testing.T) { |
| 17 | if got := flattenComment(draft); got != "hello there" { |
17 | one := `{"version":1,"document":{"type":"doc","content":[` + |
| 18 | t.Errorf("want the Draft.js block text, got %q", got) |
18 | `{"type":"paragraph","attrs":{"textAlign":"left"},"content":[` + |
| |
19 | `{"type":"text","text":"Nice artwork!"}]}]}}` |
| |
20 | if got := flattenMarkup(one); got != "Nice artwork!" { |
| |
21 | t.Errorf("want the paragraph's text, got %q", got) |
| 19 | } |
22 | } |
| 20 | |
23 | |
| 21 | // An older, plain-HTML body passes through untouched. |
24 | // Regression: DeviantArt sends "version" as a number on some bodies and a |
| 22 | html := "<b>hello</b> there" |
25 | // string on others. Modelling it as either type fails to unmarshal half of |
| 23 | if got := flattenComment(html); got != html { |
26 | // them, so it must not be modelled at all. |
| 24 | t.Errorf("want plain HTML passed through, got %q", got) |
27 | stringVersion := `{"version":"1","document":{"type":"doc","content":[` + |
| |
28 | `{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}}` |
| |
29 | if got := flattenMarkup(stringVersion); got != "hello" { |
| |
30 | t.Errorf(`want a string "version" handled the same as a numeric one, got %q`, got) |
| |
31 | } |
| |
32 | |
| |
33 | // Each block is its own line. |
| |
34 | two := `{"version":1,"document":{"type":"doc","content":[` + |
| |
35 | `{"type":"paragraph","content":[{"type":"text","text":"first"}]},` + |
| |
36 | `{"type":"paragraph","content":[{"type":"text","text":"second"}]}]}}` |
| |
37 | if got, want := flattenMarkup(two), "first\nsecond"; got != want { |
| |
38 | t.Errorf("want one line per block:\n got %q\nwant %q", got, want) |
| |
39 | } |
| |
40 | |
| |
41 | // An empty paragraph is a blank line, not something to skip. |
| |
42 | blank := `{"version":1,"document":{"type":"doc","content":[` + |
| |
43 | `{"type":"paragraph","content":[{"type":"text","text":"a"}]},` + |
| |
44 | `{"type":"paragraph"},` + |
| |
45 | `{"type":"paragraph","content":[{"type":"text","text":"b"}]}]}}` |
| |
46 | if got, want := flattenMarkup(blank), "a\n\nb"; got != want { |
| |
47 | t.Errorf("want an empty paragraph preserved as a blank line:\n got %q\nwant %q", got, want) |
| |
48 | } |
| |
49 | |
| |
50 | // A hard break is a newline within its block. |
| |
51 | brk := `{"version":1,"document":{"type":"doc","content":[` + |
| |
52 | `{"type":"paragraph","content":[{"type":"text","text":"up"},` + |
| |
53 | `{"type":"hardBreak"},{"type":"text","text":"down"}]}]}}` |
| |
54 | if got, want := flattenMarkup(brk), "up\ndown"; got != want { |
| |
55 | t.Errorf("want a hardBreak as a newline:\n got %q\nwant %q", got, want) |
| |
56 | } |
| |
57 | |
| |
58 | // Marked-up runs are separate text nodes and must be concatenated, not |
| |
59 | // separated. |
| |
60 | marks := `{"version":1,"document":{"type":"doc","content":[` + |
| |
61 | `{"type":"paragraph","content":[` + |
| |
62 | `{"type":"text","text":"plain "},` + |
| |
63 | `{"type":"text","marks":[{"type":"bold"}],"text":"bold"},` + |
| |
64 | `{"type":"text","text":" tail"}]}]}}` |
| |
65 | if got, want := flattenMarkup(marks), "plain bold tail"; got != want { |
| |
66 | t.Errorf("want marked runs concatenated:\n got %q\nwant %q", got, want) |
| |
67 | } |
| |
68 | |
| |
69 | // Text carrying a link mark still surfaces; the href does not. |
| |
70 | link := `{"version":1,"document":{"type":"doc","content":[` + |
| |
71 | `{"type":"paragraph","content":[{"type":"text","marks":[` + |
| |
72 | `{"type":"link","attrs":{"href":"https://example.com"}}],"text":"click here"}]}]}}` |
| |
73 | if got, want := flattenMarkup(link), "click here"; got != want { |
| |
74 | t.Errorf("want the link text without the href:\n got %q\nwant %q", got, want) |
| |
75 | } |
| |
76 | |
| |
77 | // Headings are blocks like any other. |
| |
78 | heading := `{"version":1,"document":{"type":"doc","content":[` + |
| |
79 | `{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Title"}]},` + |
| |
80 | `{"type":"paragraph","content":[{"type":"text","text":"body"}]}]}}` |
| |
81 | if got, want := flattenMarkup(heading), "Title\nbody"; got != want { |
| |
82 | t.Errorf("want a heading flattened as a block:\n got %q\nwant %q", got, want) |
| |
83 | } |
| |
84 | |
| |
85 | // Bodies carry HTML entities on the wire; plain text should not. |
| |
86 | entity := `{"version":1,"document":{"type":"doc","content":[` + |
| |
87 | `{"type":"paragraph","content":[{"type":"text","text":"I’d love & more"}]}]}}` |
| |
88 | if got, want := flattenMarkup(entity), "I’d love & more"; got != want { |
| |
89 | t.Errorf("want HTML entities decoded:\n got %q\nwant %q", got, want) |
| |
90 | } |
| |
91 | |
| |
92 | // A node carrying no text of its own contributes nothing. |
| |
93 | emote := `{"version":1,"document":{"type":"doc","content":[` + |
| |
94 | `{"type":"paragraph","content":[{"type":"text","text":"hi "},` + |
| |
95 | `{"type":"da-emote","attrs":{"name":":happy:"}}]}]}}` |
| |
96 | if got, want := flattenMarkup(emote), "hi "; got != want { |
| |
97 | t.Errorf("want a textless node to contribute nothing:\n got %q\nwant %q", got, want) |
| |
98 | } |
| |
99 | } |
| |
100 | |
| |
101 | // Draft.js is legacy but still served on old bodies, so the path stays. |
| |
102 | func TestFlattenMarkupDraftJS(t *testing.T) { |
| |
103 | draft := `{"blocks":[{"text":"hello there"}]}` |
| |
104 | if got := flattenMarkup(draft); got != "hello there" { |
| |
105 | t.Errorf("want the Draft.js block text, got %q", got) |
| 25 | } |
106 | } |
| 26 | |
107 | |
| 27 | // Regression: the block loop used to assign rather than accumulate, so every |
108 | // Regression: the block loop used to assign rather than accumulate, so every |
| 28 | // block but the last was silently dropped and a multi-paragraph comment came |
109 | // block but the last was silently dropped and a multi-paragraph comment came |
| 29 | // back as its closing line only. |
110 | // back as its closing line only. |
| 30 | multi := `{"blocks":[{"text":"first"},{"text":"second"},{"text":"third"}]}` |
111 | multi := `{"blocks":[{"text":"first"},{"text":"second"},{"text":"third"}]}` |
| 31 | if got, want := flattenComment(multi), "first\nsecond\nthird"; got != want { |
112 | if got, want := flattenMarkup(multi), "first\nsecond\nthird"; got != want { |
| 32 | t.Errorf("want every block, one per line:\n got %q\nwant %q", got, want) |
113 | t.Errorf("want every block, one per line:\n got %q\nwant %q", got, want) |
| 33 | } |
114 | } |
| 34 | |
115 | |
| 35 | // An empty block is a blank line in the comment, not something to skip. |
116 | // An empty block is a blank line, not something to skip. |
| 36 | blank := `{"blocks":[{"text":"first"},{"text":""},{"text":"third"}]}` |
117 | blank := `{"blocks":[{"text":"first"},{"text":""},{"text":"third"}]}` |
| 37 | if got, want := flattenComment(blank), "first\n\nthird"; got != want { |
118 | if got, want := flattenMarkup(blank), "first\n\nthird"; got != want { |
| 38 | t.Errorf("want an empty block preserved as a blank line:\n got %q\nwant %q", got, want) |
119 | t.Errorf("want an empty block preserved as a blank line:\n got %q\nwant %q", got, want) |
| 39 | } |
120 | } |
| |
121 | } |
| |
122 | |
| |
123 | func TestFlattenMarkupPassthrough(t *testing.T) { |
| |
124 | // An older, plain-HTML body passes through untouched. |
| |
125 | html := "<b>hello</b> there" |
| |
126 | if got := flattenMarkup(html); got != html { |
| |
127 | t.Errorf("want plain HTML passed through, got %q", got) |
| |
128 | } |
| |
129 | |
| |
130 | // Brace-shaped markup in no known format falls back to itself rather than to |
| |
131 | // an empty string. |
| |
132 | if got := flattenMarkup("{}"); got != "{}" { |
| |
133 | t.Errorf("want the original markup when nothing parses, got %q", got) |
| |
134 | } |
| 40 | |
135 | |
| 41 | // Brace-shaped markup that isn't a Draft.js document falls back to itself |
136 | // Well-formed JSON that is neither format is still not silently eaten. |
| 42 | // rather than to an empty string. |
137 | other := `{"something":"else"}` |
| 43 | if got := flattenComment("{}"); got != "{}" { |
138 | if got := flattenMarkup(other); got != other { |
| 44 | t.Errorf("want the original markup when there are no blocks, got %q", got) |
139 | t.Errorf("want unrecognised JSON passed through, got %q", got) |
| 45 | } |
140 | } |
| 46 | |
141 | |
| 47 | // A single brace satisfies neither end of the shape check. |
142 | // A single brace satisfies neither end of the shape check. |
| 48 | if got := flattenComment("{"); got != "{" { |
143 | if got := flattenMarkup("{"); got != "{" { |
| 49 | t.Errorf("want a lone brace passed through, got %q", got) |
144 | t.Errorf("want a lone brace passed through, got %q", got) |
| 50 | } |
145 | } |
| 51 | } |
146 | } |