@pyreon/document 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/confluence-Bd3ua1Ut.js.map +1 -1
- package/lib/csv-COrS4qdy.js.map +1 -1
- package/lib/discord-BLUnkEh9.js.map +1 -1
- package/lib/{dist-BsqdI2nY.js → dist-CYL41kqQ.js} +2 -2
- package/lib/dist-CYL41kqQ.js.map +1 -0
- package/lib/{docx-BEBOihjl.js → docx-uNAel545.js} +7 -2
- package/lib/docx-uNAel545.js.map +1 -0
- package/lib/email-D0bbfWq4.js.map +1 -1
- package/lib/{exceljs-BoIDUUaw.js → exceljs-BYETsesT.js} +314 -314
- package/lib/exceljs-BYETsesT.js.map +1 -0
- package/lib/google-chat-CkKCBUWC.js.map +1 -1
- package/lib/html-B5biprN2.js.map +1 -1
- package/lib/index.js +17 -8
- package/lib/index.js.map +1 -1
- package/lib/markdown-CdtlFGC0.js.map +1 -1
- package/lib/notion-iG2C5bEY.js.map +1 -1
- package/lib/{pdf-DIUQUEdj.js → pdf-IuBgTb3T.js} +9 -3
- package/lib/pdf-IuBgTb3T.js.map +1 -0
- package/lib/{pdfmake-DnmLxK4Q.js → pdfmake-CKMX5URW.js} +2 -4
- package/lib/pdfmake-CKMX5URW.js.map +1 -0
- package/lib/{pptx-Dd33oL3_.js → pptx-DXiMiYFM.js} +7 -2
- package/lib/pptx-DXiMiYFM.js.map +1 -0
- package/lib/{pptxgen.es-COcgXsyx.js → pptxgen.es-FsqHs8mD.js} +3 -6
- package/lib/pptxgen.es-FsqHs8mD.js.map +1 -0
- package/lib/sanitize-O_3j1mNJ.js.map +1 -1
- package/lib/slack-BI3EQwYm.js.map +1 -1
- package/lib/svg-BKxumy-p.js.map +1 -1
- package/lib/teams-Cwz9lce0.js.map +1 -1
- package/lib/telegram-gYFqyMXb.js.map +1 -1
- package/lib/text-l1XNXBOC.js.map +1 -1
- package/lib/types/index.d.ts +43 -39
- package/lib/types/index.d.ts.map +1 -1
- package/lib/{vfs_fonts-Df1kkZ4Y.js → vfs_fonts-Cap07Jg3.js} +2 -2
- package/lib/vfs_fonts-Cap07Jg3.js.map +1 -0
- package/lib/whatsapp-CjSGoOKx.js.map +1 -1
- package/lib/{xlsx-Bb5TWyXQ.js → xlsx-Cvu4LBNy.js} +8 -2
- package/lib/xlsx-Cvu4LBNy.js.map +1 -0
- package/package.json +19 -7
- package/src/builder.ts +53 -44
- package/src/download.ts +32 -36
- package/src/env.d.ts +3 -17
- package/src/index.ts +6 -8
- package/src/nodes.ts +45 -45
- package/src/render.ts +45 -118
- package/src/renderers/confluence.ts +64 -80
- package/src/renderers/csv.ts +11 -18
- package/src/renderers/discord.ts +38 -50
- package/src/renderers/docx.ts +78 -120
- package/src/renderers/email.ts +73 -92
- package/src/renderers/google-chat.ts +35 -47
- package/src/renderers/html.ts +78 -101
- package/src/renderers/markdown.ts +43 -53
- package/src/renderers/notion.ts +63 -85
- package/src/renderers/pdf.ts +92 -115
- package/src/renderers/pptx.ts +60 -66
- package/src/renderers/slack.ts +49 -61
- package/src/renderers/svg.ts +49 -63
- package/src/renderers/teams.ts +68 -80
- package/src/renderers/telegram.ts +40 -54
- package/src/renderers/text.ts +44 -66
- package/src/renderers/whatsapp.ts +34 -48
- package/src/renderers/xlsx.ts +47 -61
- package/src/sanitize.ts +21 -25
- package/src/tests/document.test.ts +1337 -1385
- package/src/tests/stress.test.ts +111 -111
- package/src/types.ts +66 -65
- package/lib/dist-BsqdI2nY.js.map +0 -1
- package/lib/docx-BEBOihjl.js.map +0 -1
- package/lib/exceljs-BoIDUUaw.js.map +0 -1
- package/lib/pdf-DIUQUEdj.js.map +0 -1
- package/lib/pdfmake-DnmLxK4Q.js.map +0 -1
- package/lib/pptx-Dd33oL3_.js.map +0 -1
- package/lib/pptxgen.es-COcgXsyx.js.map +0 -1
- package/lib/vfs_fonts-Df1kkZ4Y.js.map +0 -1
- package/lib/xlsx-Bb5TWyXQ.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
2
|
import {
|
|
3
|
+
_resetRenderers,
|
|
3
4
|
Button,
|
|
4
5
|
Code,
|
|
5
6
|
Column,
|
|
@@ -16,15 +17,14 @@ import {
|
|
|
16
17
|
PageBreak,
|
|
17
18
|
Quote,
|
|
18
19
|
Row,
|
|
20
|
+
registerRenderer,
|
|
21
|
+
render,
|
|
19
22
|
Section,
|
|
20
23
|
Spacer,
|
|
21
24
|
Table,
|
|
22
25
|
Text,
|
|
23
|
-
_resetRenderers,
|
|
24
|
-
registerRenderer,
|
|
25
|
-
render,
|
|
26
26
|
unregisterRenderer,
|
|
27
|
-
} from
|
|
27
|
+
} from "../index"
|
|
28
28
|
|
|
29
29
|
afterEach(() => {
|
|
30
30
|
_resetRenderers()
|
|
@@ -32,869 +32,844 @@ afterEach(() => {
|
|
|
32
32
|
|
|
33
33
|
// ─── Node Construction ──────────────────────────────────────────────────────
|
|
34
34
|
|
|
35
|
-
describe(
|
|
36
|
-
it(
|
|
37
|
-
const doc = Document({ title:
|
|
38
|
-
expect(doc.type).toBe(
|
|
39
|
-
expect(doc.props.title).toBe(
|
|
40
|
-
expect(doc.children).toEqual([
|
|
35
|
+
describe("node construction", () => {
|
|
36
|
+
it("Document creates a document node", () => {
|
|
37
|
+
const doc = Document({ title: "Test", children: "hello" })
|
|
38
|
+
expect(doc.type).toBe("document")
|
|
39
|
+
expect(doc.props.title).toBe("Test")
|
|
40
|
+
expect(doc.children).toEqual(["hello"])
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it(
|
|
44
|
-
const page = Page({ size:
|
|
45
|
-
expect(page.type).toBe(
|
|
46
|
-
expect(page.props.size).toBe(
|
|
43
|
+
it("Page creates a page node", () => {
|
|
44
|
+
const page = Page({ size: "A4", margin: 40, children: "content" })
|
|
45
|
+
expect(page.type).toBe("page")
|
|
46
|
+
expect(page.props.size).toBe("A4")
|
|
47
47
|
expect(page.props.margin).toBe(40)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it(
|
|
51
|
-
const section = Section({ direction:
|
|
52
|
-
expect(section.type).toBe(
|
|
53
|
-
expect(section.props.direction).toBe(
|
|
50
|
+
it("Section with direction", () => {
|
|
51
|
+
const section = Section({ direction: "row", gap: 20, children: "a" })
|
|
52
|
+
expect(section.type).toBe("section")
|
|
53
|
+
expect(section.props.direction).toBe("row")
|
|
54
54
|
expect(section.props.gap).toBe(20)
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
it(
|
|
57
|
+
it("Row and Column", () => {
|
|
58
58
|
const row = Row({
|
|
59
59
|
gap: 10,
|
|
60
|
-
children: [Column({ width:
|
|
60
|
+
children: [Column({ width: "50%", children: "left" })],
|
|
61
61
|
})
|
|
62
|
-
expect(row.type).toBe(
|
|
62
|
+
expect(row.type).toBe("row")
|
|
63
63
|
expect(row.children).toHaveLength(1)
|
|
64
64
|
const col = row.children[0]!
|
|
65
|
-
expect(typeof col).not.toBe(
|
|
66
|
-
if (typeof col !==
|
|
67
|
-
expect(col.type).toBe(
|
|
68
|
-
expect(col.props.width).toBe(
|
|
65
|
+
expect(typeof col).not.toBe("string")
|
|
66
|
+
if (typeof col !== "string") {
|
|
67
|
+
expect(col.type).toBe("column")
|
|
68
|
+
expect(col.props.width).toBe("50%")
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
73
|
-
const h = Heading({ children:
|
|
74
|
-
expect(h.type).toBe(
|
|
72
|
+
it("Heading defaults to level 1", () => {
|
|
73
|
+
const h = Heading({ children: "Title" })
|
|
74
|
+
expect(h.type).toBe("heading")
|
|
75
75
|
expect(h.props.level).toBe(1)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it(
|
|
79
|
-
const h = Heading({ level: 3, children:
|
|
78
|
+
it("Heading with custom level", () => {
|
|
79
|
+
const h = Heading({ level: 3, children: "Subtitle" })
|
|
80
80
|
expect(h.props.level).toBe(3)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it(
|
|
83
|
+
it("Text with formatting", () => {
|
|
84
84
|
const t = Text({
|
|
85
85
|
bold: true,
|
|
86
86
|
italic: true,
|
|
87
87
|
size: 14,
|
|
88
|
-
color:
|
|
89
|
-
children:
|
|
88
|
+
color: "#333",
|
|
89
|
+
children: "hello",
|
|
90
90
|
})
|
|
91
|
-
expect(t.type).toBe(
|
|
91
|
+
expect(t.type).toBe("text")
|
|
92
92
|
expect(t.props.bold).toBe(true)
|
|
93
93
|
expect(t.props.italic).toBe(true)
|
|
94
94
|
expect(t.props.size).toBe(14)
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
-
it(
|
|
98
|
-
const l = Link({ href:
|
|
99
|
-
expect(l.type).toBe(
|
|
100
|
-
expect(l.props.href).toBe(
|
|
97
|
+
it("Link", () => {
|
|
98
|
+
const l = Link({ href: "https://example.com", children: "click" })
|
|
99
|
+
expect(l.type).toBe("link")
|
|
100
|
+
expect(l.props.href).toBe("https://example.com")
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
it(
|
|
103
|
+
it("Image with all props", () => {
|
|
104
104
|
const img = Image({
|
|
105
|
-
src:
|
|
105
|
+
src: "/logo.png",
|
|
106
106
|
width: 100,
|
|
107
107
|
height: 50,
|
|
108
|
-
alt:
|
|
109
|
-
caption:
|
|
108
|
+
alt: "Logo",
|
|
109
|
+
caption: "Company logo",
|
|
110
110
|
})
|
|
111
|
-
expect(img.type).toBe(
|
|
112
|
-
expect(img.props.src).toBe(
|
|
111
|
+
expect(img.type).toBe("image")
|
|
112
|
+
expect(img.props.src).toBe("/logo.png")
|
|
113
113
|
expect(img.props.width).toBe(100)
|
|
114
|
-
expect(img.props.caption).toBe(
|
|
114
|
+
expect(img.props.caption).toBe("Company logo")
|
|
115
115
|
expect(img.children).toEqual([])
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
-
it(
|
|
118
|
+
it("Table with columns and rows", () => {
|
|
119
119
|
const t = Table({
|
|
120
|
-
columns: [
|
|
121
|
-
rows: [[
|
|
120
|
+
columns: ["Name", { header: "Price", align: "right" }],
|
|
121
|
+
rows: [["Widget", "$10"]],
|
|
122
122
|
striped: true,
|
|
123
123
|
})
|
|
124
|
-
expect(t.type).toBe(
|
|
124
|
+
expect(t.type).toBe("table")
|
|
125
125
|
expect(t.props.columns).toHaveLength(2)
|
|
126
126
|
expect(t.props.rows).toHaveLength(1)
|
|
127
127
|
expect(t.props.striped).toBe(true)
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
it(
|
|
130
|
+
it("List with items", () => {
|
|
131
131
|
const l = List({
|
|
132
132
|
ordered: true,
|
|
133
|
-
children: [ListItem({ children:
|
|
133
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
134
134
|
})
|
|
135
|
-
expect(l.type).toBe(
|
|
135
|
+
expect(l.type).toBe("list")
|
|
136
136
|
expect(l.props.ordered).toBe(true)
|
|
137
137
|
expect(l.children).toHaveLength(2)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
it(
|
|
141
|
-
const c = Code({ language:
|
|
142
|
-
expect(c.type).toBe(
|
|
143
|
-
expect(c.props.language).toBe(
|
|
140
|
+
it("Code", () => {
|
|
141
|
+
const c = Code({ language: "typescript", children: "const x = 1" })
|
|
142
|
+
expect(c.type).toBe("code")
|
|
143
|
+
expect(c.props.language).toBe("typescript")
|
|
144
144
|
})
|
|
145
145
|
|
|
146
|
-
it(
|
|
147
|
-
const d = Divider({ color:
|
|
148
|
-
expect(d.type).toBe(
|
|
149
|
-
expect(d.props.color).toBe(
|
|
146
|
+
it("Divider", () => {
|
|
147
|
+
const d = Divider({ color: "#ccc", thickness: 2 })
|
|
148
|
+
expect(d.type).toBe("divider")
|
|
149
|
+
expect(d.props.color).toBe("#ccc")
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
it(
|
|
152
|
+
it("Divider with defaults", () => {
|
|
153
153
|
const d = Divider()
|
|
154
|
-
expect(d.type).toBe(
|
|
154
|
+
expect(d.type).toBe("divider")
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
it(
|
|
157
|
+
it("Spacer", () => {
|
|
158
158
|
const s = Spacer({ height: 30 })
|
|
159
|
-
expect(s.type).toBe(
|
|
159
|
+
expect(s.type).toBe("spacer")
|
|
160
160
|
expect(s.props.height).toBe(30)
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
-
it(
|
|
164
|
-
const b = Button({ href:
|
|
165
|
-
expect(b.type).toBe(
|
|
166
|
-
expect(b.props.href).toBe(
|
|
167
|
-
expect(b.props.background).toBe(
|
|
163
|
+
it("Button", () => {
|
|
164
|
+
const b = Button({ href: "/pay", background: "#4f46e5", children: "Pay" })
|
|
165
|
+
expect(b.type).toBe("button")
|
|
166
|
+
expect(b.props.href).toBe("/pay")
|
|
167
|
+
expect(b.props.background).toBe("#4f46e5")
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
it(
|
|
171
|
-
const q = Quote({ borderColor:
|
|
172
|
-
expect(q.type).toBe(
|
|
173
|
-
expect(q.props.borderColor).toBe(
|
|
170
|
+
it("Quote", () => {
|
|
171
|
+
const q = Quote({ borderColor: "#blue", children: "wise words" })
|
|
172
|
+
expect(q.type).toBe("quote")
|
|
173
|
+
expect(q.props.borderColor).toBe("#blue")
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
it(
|
|
177
|
-
expect(isDocNode(Heading({ children:
|
|
176
|
+
it("isDocNode returns true for nodes", () => {
|
|
177
|
+
expect(isDocNode(Heading({ children: "hi" }))).toBe(true)
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
-
it(
|
|
181
|
-
expect(isDocNode(
|
|
180
|
+
it("isDocNode returns false for non-nodes", () => {
|
|
181
|
+
expect(isDocNode("string")).toBe(false)
|
|
182
182
|
expect(isDocNode(null)).toBe(false)
|
|
183
183
|
expect(isDocNode(42)).toBe(false)
|
|
184
184
|
expect(isDocNode({})).toBe(false)
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
it(
|
|
187
|
+
it("normalizes nested children", () => {
|
|
188
188
|
const doc = Document({
|
|
189
|
-
children: [
|
|
190
|
-
Heading({ children: 'A' }),
|
|
191
|
-
[Text({ children: 'B' }), Text({ children: 'C' })],
|
|
192
|
-
],
|
|
189
|
+
children: [Heading({ children: "A" }), [Text({ children: "B" }), Text({ children: "C" })]],
|
|
193
190
|
})
|
|
194
191
|
expect(doc.children).toHaveLength(3)
|
|
195
192
|
})
|
|
196
193
|
|
|
197
|
-
it(
|
|
198
|
-
const doc = Document({ children: [null, undefined, false,
|
|
199
|
-
expect(doc.children).toEqual([
|
|
194
|
+
it("handles null/undefined/false children", () => {
|
|
195
|
+
const doc = Document({ children: [null, undefined, false, "text"] })
|
|
196
|
+
expect(doc.children).toEqual(["text"])
|
|
200
197
|
})
|
|
201
198
|
|
|
202
|
-
it(
|
|
199
|
+
it("converts numbers to strings in children", () => {
|
|
203
200
|
const t = Text({ children: 42 as unknown as string })
|
|
204
|
-
expect(t.children).toEqual([
|
|
201
|
+
expect(t.children).toEqual(["42"])
|
|
205
202
|
})
|
|
206
203
|
})
|
|
207
204
|
|
|
208
205
|
// ─── HTML Renderer ──────────────────────────────────────────────────────────
|
|
209
206
|
|
|
210
|
-
describe(
|
|
211
|
-
it(
|
|
207
|
+
describe("HTML renderer", () => {
|
|
208
|
+
it("renders a simple document", async () => {
|
|
212
209
|
const doc = Document({
|
|
213
|
-
title:
|
|
214
|
-
children: Page({ children: Heading({ children:
|
|
210
|
+
title: "Test",
|
|
211
|
+
children: Page({ children: Heading({ children: "Hello" }) }),
|
|
215
212
|
})
|
|
216
|
-
const html = (await render(doc,
|
|
217
|
-
expect(html).toContain(
|
|
218
|
-
expect(html).toContain(
|
|
219
|
-
expect(html).toContain(
|
|
220
|
-
expect(html).toContain(
|
|
213
|
+
const html = (await render(doc, "html")) as string
|
|
214
|
+
expect(html).toContain("<!DOCTYPE html>")
|
|
215
|
+
expect(html).toContain("<title>Test</title>")
|
|
216
|
+
expect(html).toContain("<h1")
|
|
217
|
+
expect(html).toContain("Hello")
|
|
221
218
|
})
|
|
222
219
|
|
|
223
|
-
it(
|
|
220
|
+
it("renders text with formatting", async () => {
|
|
224
221
|
const doc = Document({
|
|
225
222
|
children: Text({
|
|
226
223
|
bold: true,
|
|
227
|
-
color:
|
|
224
|
+
color: "#f00",
|
|
228
225
|
size: 20,
|
|
229
|
-
align:
|
|
230
|
-
children:
|
|
226
|
+
align: "center",
|
|
227
|
+
children: "Bold Red",
|
|
231
228
|
}),
|
|
232
229
|
})
|
|
233
|
-
const html = (await render(doc,
|
|
234
|
-
expect(html).toContain(
|
|
235
|
-
expect(html).toContain(
|
|
236
|
-
expect(html).toContain(
|
|
237
|
-
expect(html).toContain(
|
|
230
|
+
const html = (await render(doc, "html")) as string
|
|
231
|
+
expect(html).toContain("font-weight:bold")
|
|
232
|
+
expect(html).toContain("color:#f00")
|
|
233
|
+
expect(html).toContain("font-size:20px")
|
|
234
|
+
expect(html).toContain("text-align:center")
|
|
238
235
|
})
|
|
239
236
|
|
|
240
|
-
it(
|
|
237
|
+
it("renders a table", async () => {
|
|
241
238
|
const doc = Document({
|
|
242
239
|
children: Table({
|
|
243
|
-
columns: [
|
|
240
|
+
columns: ["Name", { header: "Price", align: "right" }],
|
|
244
241
|
rows: [
|
|
245
|
-
[
|
|
246
|
-
[
|
|
242
|
+
["Widget", "$10"],
|
|
243
|
+
["Gadget", "$20"],
|
|
247
244
|
],
|
|
248
245
|
striped: true,
|
|
249
|
-
headerStyle: { background:
|
|
246
|
+
headerStyle: { background: "#000", color: "#fff" },
|
|
250
247
|
}),
|
|
251
248
|
})
|
|
252
|
-
const html = (await render(doc,
|
|
253
|
-
expect(html).toContain(
|
|
254
|
-
expect(html).toContain(
|
|
255
|
-
expect(html).toContain(
|
|
256
|
-
expect(html).toContain(
|
|
257
|
-
expect(html).toContain(
|
|
249
|
+
const html = (await render(doc, "html")) as string
|
|
250
|
+
expect(html).toContain("<table")
|
|
251
|
+
expect(html).toContain("Widget")
|
|
252
|
+
expect(html).toContain("$10")
|
|
253
|
+
expect(html).toContain("background:#000")
|
|
254
|
+
expect(html).toContain("color:#fff")
|
|
258
255
|
})
|
|
259
256
|
|
|
260
|
-
it(
|
|
257
|
+
it("renders an image with caption", async () => {
|
|
261
258
|
const doc = Document({
|
|
262
259
|
children: Image({
|
|
263
|
-
src:
|
|
260
|
+
src: "/img.png",
|
|
264
261
|
width: 200,
|
|
265
|
-
alt:
|
|
266
|
-
caption:
|
|
262
|
+
alt: "Photo",
|
|
263
|
+
caption: "A photo",
|
|
267
264
|
}),
|
|
268
265
|
})
|
|
269
|
-
const html = (await render(doc,
|
|
270
|
-
expect(html).toContain(
|
|
266
|
+
const html = (await render(doc, "html")) as string
|
|
267
|
+
expect(html).toContain("<img")
|
|
271
268
|
expect(html).toContain('src="/img.png"')
|
|
272
|
-
expect(html).toContain(
|
|
273
|
-
expect(html).toContain(
|
|
269
|
+
expect(html).toContain("<figcaption>")
|
|
270
|
+
expect(html).toContain("A photo")
|
|
274
271
|
})
|
|
275
272
|
|
|
276
|
-
it(
|
|
273
|
+
it("renders a link", async () => {
|
|
277
274
|
const doc = Document({
|
|
278
|
-
children: Link({ href:
|
|
275
|
+
children: Link({ href: "https://example.com", children: "Click me" }),
|
|
279
276
|
})
|
|
280
|
-
const html = (await render(doc,
|
|
277
|
+
const html = (await render(doc, "html")) as string
|
|
281
278
|
expect(html).toContain('href="https://example.com"')
|
|
282
|
-
expect(html).toContain(
|
|
279
|
+
expect(html).toContain("Click me")
|
|
283
280
|
})
|
|
284
281
|
|
|
285
|
-
it(
|
|
282
|
+
it("renders a list", async () => {
|
|
286
283
|
const doc = Document({
|
|
287
284
|
children: List({
|
|
288
285
|
ordered: true,
|
|
289
|
-
children: [
|
|
290
|
-
ListItem({ children: 'one' }),
|
|
291
|
-
ListItem({ children: 'two' }),
|
|
292
|
-
],
|
|
286
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
293
287
|
}),
|
|
294
288
|
})
|
|
295
|
-
const html = (await render(doc,
|
|
296
|
-
expect(html).toContain(
|
|
297
|
-
expect(html).toContain(
|
|
289
|
+
const html = (await render(doc, "html")) as string
|
|
290
|
+
expect(html).toContain("<ol>")
|
|
291
|
+
expect(html).toContain("<li>one</li>")
|
|
298
292
|
})
|
|
299
293
|
|
|
300
|
-
it(
|
|
301
|
-
const doc = Document({ children: Code({ children:
|
|
302
|
-
const html = (await render(doc,
|
|
303
|
-
expect(html).toContain(
|
|
304
|
-
expect(html).toContain(
|
|
305
|
-
expect(html).toContain(
|
|
294
|
+
it("renders code blocks", async () => {
|
|
295
|
+
const doc = Document({ children: Code({ children: "const x = 1" }) })
|
|
296
|
+
const html = (await render(doc, "html")) as string
|
|
297
|
+
expect(html).toContain("<pre")
|
|
298
|
+
expect(html).toContain("<code>")
|
|
299
|
+
expect(html).toContain("const x = 1")
|
|
306
300
|
})
|
|
307
301
|
|
|
308
|
-
it(
|
|
309
|
-
const doc = Document({ children: Divider({ color:
|
|
310
|
-
const html = (await render(doc,
|
|
311
|
-
expect(html).toContain(
|
|
312
|
-
expect(html).toContain(
|
|
302
|
+
it("renders divider", async () => {
|
|
303
|
+
const doc = Document({ children: Divider({ color: "#ccc", thickness: 2 }) })
|
|
304
|
+
const html = (await render(doc, "html")) as string
|
|
305
|
+
expect(html).toContain("<hr")
|
|
306
|
+
expect(html).toContain("2px solid #ccc")
|
|
313
307
|
})
|
|
314
308
|
|
|
315
|
-
it(
|
|
309
|
+
it("renders spacer", async () => {
|
|
316
310
|
const doc = Document({ children: Spacer({ height: 30 }) })
|
|
317
|
-
const html = (await render(doc,
|
|
318
|
-
expect(html).toContain(
|
|
311
|
+
const html = (await render(doc, "html")) as string
|
|
312
|
+
expect(html).toContain("height:30px")
|
|
319
313
|
})
|
|
320
314
|
|
|
321
|
-
it(
|
|
315
|
+
it("renders button", async () => {
|
|
322
316
|
const doc = Document({
|
|
323
317
|
children: Button({
|
|
324
|
-
href:
|
|
325
|
-
background:
|
|
326
|
-
color:
|
|
327
|
-
children:
|
|
318
|
+
href: "/pay",
|
|
319
|
+
background: "#4f46e5",
|
|
320
|
+
color: "#fff",
|
|
321
|
+
children: "Pay",
|
|
328
322
|
}),
|
|
329
323
|
})
|
|
330
|
-
const html = (await render(doc,
|
|
324
|
+
const html = (await render(doc, "html")) as string
|
|
331
325
|
expect(html).toContain('href="/pay"')
|
|
332
|
-
expect(html).toContain(
|
|
333
|
-
expect(html).toContain(
|
|
326
|
+
expect(html).toContain("background:#4f46e5")
|
|
327
|
+
expect(html).toContain("Pay")
|
|
334
328
|
})
|
|
335
329
|
|
|
336
|
-
it(
|
|
337
|
-
const doc = Document({ children: Quote({ children:
|
|
338
|
-
const html = (await render(doc,
|
|
339
|
-
expect(html).toContain(
|
|
340
|
-
expect(html).toContain(
|
|
330
|
+
it("renders blockquote", async () => {
|
|
331
|
+
const doc = Document({ children: Quote({ children: "A wise quote" }) })
|
|
332
|
+
const html = (await render(doc, "html")) as string
|
|
333
|
+
expect(html).toContain("<blockquote")
|
|
334
|
+
expect(html).toContain("A wise quote")
|
|
341
335
|
})
|
|
342
336
|
|
|
343
|
-
it(
|
|
337
|
+
it("renders section with row direction", async () => {
|
|
344
338
|
const doc = Document({
|
|
345
339
|
children: Section({
|
|
346
|
-
direction:
|
|
340
|
+
direction: "row",
|
|
347
341
|
gap: 20,
|
|
348
|
-
background:
|
|
349
|
-
children: [Text({ children:
|
|
342
|
+
background: "#f5f5f5",
|
|
343
|
+
children: [Text({ children: "A" }), Text({ children: "B" })],
|
|
350
344
|
}),
|
|
351
345
|
})
|
|
352
|
-
const html = (await render(doc,
|
|
353
|
-
expect(html).toContain(
|
|
354
|
-
expect(html).toContain(
|
|
346
|
+
const html = (await render(doc, "html")) as string
|
|
347
|
+
expect(html).toContain("display:flex")
|
|
348
|
+
expect(html).toContain("flex-direction:row")
|
|
355
349
|
})
|
|
356
350
|
|
|
357
|
-
it(
|
|
351
|
+
it("renders image with center alignment", async () => {
|
|
358
352
|
const doc = Document({
|
|
359
|
-
children: Image({ src:
|
|
353
|
+
children: Image({ src: "/img.png", align: "center" }),
|
|
360
354
|
})
|
|
361
|
-
const html = (await render(doc,
|
|
362
|
-
expect(html).toContain(
|
|
355
|
+
const html = (await render(doc, "html")) as string
|
|
356
|
+
expect(html).toContain("margin:0 auto")
|
|
363
357
|
})
|
|
364
358
|
|
|
365
|
-
it(
|
|
359
|
+
it("renders table with bordered option", async () => {
|
|
366
360
|
const doc = Document({
|
|
367
|
-
children: Table({ columns: [
|
|
361
|
+
children: Table({ columns: ["A"], rows: [["1"]], bordered: true }),
|
|
368
362
|
})
|
|
369
|
-
const html = (await render(doc,
|
|
370
|
-
expect(html).toContain(
|
|
363
|
+
const html = (await render(doc, "html")) as string
|
|
364
|
+
expect(html).toContain("border:1px solid #ddd")
|
|
371
365
|
})
|
|
372
366
|
|
|
373
|
-
it(
|
|
367
|
+
it("renders table with caption", async () => {
|
|
374
368
|
const doc = Document({
|
|
375
|
-
children: Table({ columns: [
|
|
369
|
+
children: Table({ columns: ["A"], rows: [["1"]], caption: "My Table" }),
|
|
376
370
|
})
|
|
377
|
-
const html = (await render(doc,
|
|
378
|
-
expect(html).toContain(
|
|
371
|
+
const html = (await render(doc, "html")) as string
|
|
372
|
+
expect(html).toContain("<caption>My Table</caption>")
|
|
379
373
|
})
|
|
380
374
|
|
|
381
|
-
it(
|
|
375
|
+
it("escapes HTML in text", async () => {
|
|
382
376
|
const doc = Document({
|
|
383
|
-
children: Text({ children:
|
|
377
|
+
children: Text({ children: "<script>alert(1)</script>" }),
|
|
384
378
|
})
|
|
385
|
-
const html = (await render(doc,
|
|
386
|
-
expect(html).not.toContain(
|
|
387
|
-
expect(html).toContain(
|
|
379
|
+
const html = (await render(doc, "html")) as string
|
|
380
|
+
expect(html).not.toContain("<script>")
|
|
381
|
+
expect(html).toContain("<script>")
|
|
388
382
|
})
|
|
389
383
|
|
|
390
|
-
it(
|
|
384
|
+
it("renders text with underline and strikethrough", async () => {
|
|
391
385
|
const ul = Document({
|
|
392
|
-
children: Text({ underline: true, children:
|
|
386
|
+
children: Text({ underline: true, children: "underlined" }),
|
|
393
387
|
})
|
|
394
388
|
const st = Document({
|
|
395
|
-
children: Text({ strikethrough: true, children:
|
|
389
|
+
children: Text({ strikethrough: true, children: "struck" }),
|
|
396
390
|
})
|
|
397
|
-
expect((await render(ul,
|
|
398
|
-
|
|
399
|
-
)
|
|
400
|
-
expect((await render(st, 'html')) as string).toContain(
|
|
401
|
-
'text-decoration:line-through',
|
|
402
|
-
)
|
|
391
|
+
expect((await render(ul, "html")) as string).toContain("text-decoration:underline")
|
|
392
|
+
expect((await render(st, "html")) as string).toContain("text-decoration:line-through")
|
|
403
393
|
})
|
|
404
394
|
|
|
405
|
-
it(
|
|
406
|
-
const doc = Document({ children: Image({ src:
|
|
407
|
-
const html = (await render(doc,
|
|
408
|
-
expect(html).toContain(
|
|
395
|
+
it("renders image with right alignment", async () => {
|
|
396
|
+
const doc = Document({ children: Image({ src: "/x.png", align: "right" }) })
|
|
397
|
+
const html = (await render(doc, "html")) as string
|
|
398
|
+
expect(html).toContain("margin-left:auto")
|
|
409
399
|
})
|
|
410
400
|
})
|
|
411
401
|
|
|
412
402
|
// ─── Email Renderer ─────────────────────────────────────────────────────────
|
|
413
403
|
|
|
414
|
-
describe(
|
|
415
|
-
it(
|
|
404
|
+
describe("email renderer", () => {
|
|
405
|
+
it("renders email-safe HTML", async () => {
|
|
416
406
|
const doc = Document({
|
|
417
|
-
title:
|
|
418
|
-
children: [
|
|
419
|
-
Heading({ children: 'Hello!' }),
|
|
420
|
-
Text({ children: 'Welcome to our service.' }),
|
|
421
|
-
],
|
|
407
|
+
title: "Welcome",
|
|
408
|
+
children: [Heading({ children: "Hello!" }), Text({ children: "Welcome to our service." })],
|
|
422
409
|
})
|
|
423
|
-
const html = (await render(doc,
|
|
424
|
-
expect(html).toContain(
|
|
425
|
-
expect(html).toContain(
|
|
426
|
-
expect(html).toContain(
|
|
410
|
+
const html = (await render(doc, "email")) as string
|
|
411
|
+
expect(html).toContain("<!DOCTYPE html>")
|
|
412
|
+
expect(html).toContain("max-width:600px")
|
|
413
|
+
expect(html).toContain("Hello!")
|
|
427
414
|
// Should have Outlook conditional comments
|
|
428
|
-
expect(html).toContain(
|
|
415
|
+
expect(html).toContain("<!--[if mso]>")
|
|
429
416
|
})
|
|
430
417
|
|
|
431
|
-
it(
|
|
418
|
+
it("renders bulletproof buttons", async () => {
|
|
432
419
|
const doc = Document({
|
|
433
420
|
children: Button({
|
|
434
|
-
href:
|
|
435
|
-
background:
|
|
436
|
-
children:
|
|
421
|
+
href: "/pay",
|
|
422
|
+
background: "#4f46e5",
|
|
423
|
+
children: "Pay Now",
|
|
437
424
|
}),
|
|
438
425
|
})
|
|
439
|
-
const html = (await render(doc,
|
|
426
|
+
const html = (await render(doc, "email")) as string
|
|
440
427
|
// VML for Outlook
|
|
441
|
-
expect(html).toContain(
|
|
428
|
+
expect(html).toContain("v:roundrect")
|
|
442
429
|
// CSS for others
|
|
443
|
-
expect(html).toContain(
|
|
444
|
-
expect(html).toContain(
|
|
430
|
+
expect(html).toContain("background-color:#4f46e5")
|
|
431
|
+
expect(html).toContain("Pay Now")
|
|
445
432
|
})
|
|
446
433
|
|
|
447
|
-
it(
|
|
434
|
+
it("renders table with inline styles", async () => {
|
|
448
435
|
const doc = Document({
|
|
449
436
|
children: Table({
|
|
450
|
-
columns: [
|
|
451
|
-
rows: [[
|
|
452
|
-
headerStyle: { background:
|
|
437
|
+
columns: ["Name", "Price"],
|
|
438
|
+
rows: [["Widget", "$10"]],
|
|
439
|
+
headerStyle: { background: "#000", color: "#fff" },
|
|
453
440
|
}),
|
|
454
441
|
})
|
|
455
|
-
const html = (await render(doc,
|
|
456
|
-
expect(html).toContain(
|
|
457
|
-
expect(html).toContain(
|
|
458
|
-
expect(html).toContain(
|
|
442
|
+
const html = (await render(doc, "email")) as string
|
|
443
|
+
expect(html).toContain("background-color:#000")
|
|
444
|
+
expect(html).toContain("color:#fff")
|
|
445
|
+
expect(html).toContain("Widget")
|
|
459
446
|
})
|
|
460
447
|
|
|
461
|
-
it(
|
|
448
|
+
it("renders section with row direction using tables", async () => {
|
|
462
449
|
const doc = Document({
|
|
463
450
|
children: Section({
|
|
464
|
-
direction:
|
|
465
|
-
children: [Text({ children:
|
|
451
|
+
direction: "row",
|
|
452
|
+
children: [Text({ children: "Left" }), Text({ children: "Right" })],
|
|
466
453
|
}),
|
|
467
454
|
})
|
|
468
|
-
const html = (await render(doc,
|
|
455
|
+
const html = (await render(doc, "email")) as string
|
|
469
456
|
// Should use table layout, not flexbox
|
|
470
|
-
expect(html).not.toContain(
|
|
471
|
-
expect(html).toContain(
|
|
472
|
-
expect(html).toContain(
|
|
473
|
-
expect(html).toContain(
|
|
457
|
+
expect(html).not.toContain("display:flex")
|
|
458
|
+
expect(html).toContain("<table")
|
|
459
|
+
expect(html).toContain("Left")
|
|
460
|
+
expect(html).toContain("Right")
|
|
474
461
|
})
|
|
475
462
|
|
|
476
|
-
it(
|
|
463
|
+
it("renders divider using table", async () => {
|
|
477
464
|
const doc = Document({ children: Divider() })
|
|
478
|
-
const html = (await render(doc,
|
|
479
|
-
expect(html).toContain(
|
|
465
|
+
const html = (await render(doc, "email")) as string
|
|
466
|
+
expect(html).toContain("border-top:1px solid")
|
|
480
467
|
})
|
|
481
468
|
|
|
482
|
-
it(
|
|
483
|
-
const doc = Document({ children: Quote({ children:
|
|
484
|
-
const html = (await render(doc,
|
|
485
|
-
expect(html).toContain(
|
|
469
|
+
it("renders quote using table with border-left", async () => {
|
|
470
|
+
const doc = Document({ children: Quote({ children: "A quote" }) })
|
|
471
|
+
const html = (await render(doc, "email")) as string
|
|
472
|
+
expect(html).toContain("border-left:4px solid")
|
|
486
473
|
})
|
|
487
474
|
})
|
|
488
475
|
|
|
489
476
|
// ─── Markdown Renderer ──────────────────────────────────────────────────────
|
|
490
477
|
|
|
491
|
-
describe(
|
|
492
|
-
it(
|
|
478
|
+
describe("markdown renderer", () => {
|
|
479
|
+
it("renders headings with # prefix", async () => {
|
|
493
480
|
const doc = Document({
|
|
494
481
|
children: [
|
|
495
|
-
Heading({ level: 1, children:
|
|
496
|
-
Heading({ level: 3, children:
|
|
482
|
+
Heading({ level: 1, children: "Title" }),
|
|
483
|
+
Heading({ level: 3, children: "Subtitle" }),
|
|
497
484
|
],
|
|
498
485
|
})
|
|
499
|
-
const md = (await render(doc,
|
|
500
|
-
expect(md).toContain(
|
|
501
|
-
expect(md).toContain(
|
|
486
|
+
const md = (await render(doc, "md")) as string
|
|
487
|
+
expect(md).toContain("# Title")
|
|
488
|
+
expect(md).toContain("### Subtitle")
|
|
502
489
|
})
|
|
503
490
|
|
|
504
|
-
it(
|
|
491
|
+
it("renders bold and italic text", async () => {
|
|
505
492
|
const doc = Document({
|
|
506
493
|
children: [
|
|
507
|
-
Text({ bold: true, children:
|
|
508
|
-
Text({ italic: true, children:
|
|
509
|
-
Text({ strikethrough: true, children:
|
|
494
|
+
Text({ bold: true, children: "bold" }),
|
|
495
|
+
Text({ italic: true, children: "italic" }),
|
|
496
|
+
Text({ strikethrough: true, children: "struck" }),
|
|
510
497
|
],
|
|
511
498
|
})
|
|
512
|
-
const md = (await render(doc,
|
|
513
|
-
expect(md).toContain(
|
|
514
|
-
expect(md).toContain(
|
|
515
|
-
expect(md).toContain(
|
|
499
|
+
const md = (await render(doc, "md")) as string
|
|
500
|
+
expect(md).toContain("**bold**")
|
|
501
|
+
expect(md).toContain("*italic*")
|
|
502
|
+
expect(md).toContain("~~struck~~")
|
|
516
503
|
})
|
|
517
504
|
|
|
518
|
-
it(
|
|
505
|
+
it("renders tables as pipe tables", async () => {
|
|
519
506
|
const doc = Document({
|
|
520
507
|
children: Table({
|
|
521
|
-
columns: [
|
|
522
|
-
rows: [[
|
|
508
|
+
columns: ["Name", { header: "Price", align: "right" }],
|
|
509
|
+
rows: [["Widget", "$10"]],
|
|
523
510
|
}),
|
|
524
511
|
})
|
|
525
|
-
const md = (await render(doc,
|
|
526
|
-
expect(md).toContain(
|
|
527
|
-
expect(md).toContain(
|
|
528
|
-
expect(md).toContain(
|
|
512
|
+
const md = (await render(doc, "md")) as string
|
|
513
|
+
expect(md).toContain("| Name | Price |")
|
|
514
|
+
expect(md).toContain("| --- | ---: |")
|
|
515
|
+
expect(md).toContain("| Widget | $10 |")
|
|
529
516
|
})
|
|
530
517
|
|
|
531
|
-
it(
|
|
518
|
+
it("renders links in markdown format", async () => {
|
|
532
519
|
const doc = Document({
|
|
533
|
-
children: Link({ href:
|
|
520
|
+
children: Link({ href: "https://example.com", children: "click" }),
|
|
534
521
|
})
|
|
535
|
-
const md = (await render(doc,
|
|
536
|
-
expect(md).toContain(
|
|
522
|
+
const md = (await render(doc, "md")) as string
|
|
523
|
+
expect(md).toContain("[click](https://example.com)")
|
|
537
524
|
})
|
|
538
525
|
|
|
539
|
-
it(
|
|
526
|
+
it("renders images", async () => {
|
|
540
527
|
const doc = Document({
|
|
541
|
-
children: Image({ src:
|
|
528
|
+
children: Image({ src: "/img.png", alt: "Photo", caption: "A photo" }),
|
|
542
529
|
})
|
|
543
|
-
const md = (await render(doc,
|
|
544
|
-
expect(md).toContain(
|
|
545
|
-
expect(md).toContain(
|
|
530
|
+
const md = (await render(doc, "md")) as string
|
|
531
|
+
expect(md).toContain("")
|
|
532
|
+
expect(md).toContain("*A photo*")
|
|
546
533
|
})
|
|
547
534
|
|
|
548
|
-
it(
|
|
535
|
+
it("renders code blocks with language", async () => {
|
|
549
536
|
const doc = Document({
|
|
550
|
-
children: Code({ language:
|
|
537
|
+
children: Code({ language: "typescript", children: "const x = 1" }),
|
|
551
538
|
})
|
|
552
|
-
const md = (await render(doc,
|
|
553
|
-
expect(md).toContain(
|
|
554
|
-
expect(md).toContain(
|
|
555
|
-
expect(md).toContain(
|
|
539
|
+
const md = (await render(doc, "md")) as string
|
|
540
|
+
expect(md).toContain("```typescript")
|
|
541
|
+
expect(md).toContain("const x = 1")
|
|
542
|
+
expect(md).toContain("```")
|
|
556
543
|
})
|
|
557
544
|
|
|
558
|
-
it(
|
|
545
|
+
it("renders ordered and unordered lists", async () => {
|
|
559
546
|
const ol = Document({
|
|
560
547
|
children: List({
|
|
561
548
|
ordered: true,
|
|
562
|
-
children: [
|
|
563
|
-
ListItem({ children: 'first' }),
|
|
564
|
-
ListItem({ children: 'second' }),
|
|
565
|
-
],
|
|
549
|
+
children: [ListItem({ children: "first" }), ListItem({ children: "second" })],
|
|
566
550
|
}),
|
|
567
551
|
})
|
|
568
552
|
const ul = Document({
|
|
569
553
|
children: List({
|
|
570
|
-
children: [ListItem({ children:
|
|
554
|
+
children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
|
|
571
555
|
}),
|
|
572
556
|
})
|
|
573
|
-
const orderedMd = (await render(ol,
|
|
574
|
-
const unorderedMd = (await render(ul,
|
|
575
|
-
expect(orderedMd).toContain(
|
|
576
|
-
expect(orderedMd).toContain(
|
|
577
|
-
expect(unorderedMd).toContain(
|
|
578
|
-
expect(unorderedMd).toContain(
|
|
557
|
+
const orderedMd = (await render(ol, "md")) as string
|
|
558
|
+
const unorderedMd = (await render(ul, "md")) as string
|
|
559
|
+
expect(orderedMd).toContain("1. first")
|
|
560
|
+
expect(orderedMd).toContain("2. second")
|
|
561
|
+
expect(unorderedMd).toContain("- a")
|
|
562
|
+
expect(unorderedMd).toContain("- b")
|
|
579
563
|
})
|
|
580
564
|
|
|
581
|
-
it(
|
|
565
|
+
it("renders divider as ---", async () => {
|
|
582
566
|
const doc = Document({ children: Divider() })
|
|
583
|
-
const md = (await render(doc,
|
|
584
|
-
expect(md).toContain(
|
|
567
|
+
const md = (await render(doc, "md")) as string
|
|
568
|
+
expect(md).toContain("---")
|
|
585
569
|
})
|
|
586
570
|
|
|
587
|
-
it(
|
|
571
|
+
it("renders button as link", async () => {
|
|
588
572
|
const doc = Document({
|
|
589
|
-
children: Button({ href:
|
|
573
|
+
children: Button({ href: "/pay", children: "Pay" }),
|
|
590
574
|
})
|
|
591
|
-
const md = (await render(doc,
|
|
592
|
-
expect(md).toContain(
|
|
575
|
+
const md = (await render(doc, "md")) as string
|
|
576
|
+
expect(md).toContain("[Pay](/pay)")
|
|
593
577
|
})
|
|
594
578
|
|
|
595
|
-
it(
|
|
596
|
-
const doc = Document({ children: Quote({ children:
|
|
597
|
-
const md = (await render(doc,
|
|
598
|
-
expect(md).toContain(
|
|
579
|
+
it("renders quote with >", async () => {
|
|
580
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
581
|
+
const md = (await render(doc, "md")) as string
|
|
582
|
+
expect(md).toContain("> wise")
|
|
599
583
|
})
|
|
600
584
|
|
|
601
|
-
it(
|
|
585
|
+
it("renders table with caption", async () => {
|
|
602
586
|
const doc = Document({
|
|
603
|
-
children: Table({ columns: [
|
|
587
|
+
children: Table({ columns: ["A"], rows: [["1"]], caption: "My Table" }),
|
|
604
588
|
})
|
|
605
|
-
const md = (await render(doc,
|
|
606
|
-
expect(md).toContain(
|
|
589
|
+
const md = (await render(doc, "md")) as string
|
|
590
|
+
expect(md).toContain("*My Table*")
|
|
607
591
|
})
|
|
608
592
|
})
|
|
609
593
|
|
|
610
594
|
// ─── Text Renderer ──────────────────────────────────────────────────────────
|
|
611
595
|
|
|
612
|
-
describe(
|
|
613
|
-
it(
|
|
596
|
+
describe("text renderer", () => {
|
|
597
|
+
it("renders headings with underlines", async () => {
|
|
614
598
|
const doc = Document({
|
|
615
|
-
children: [
|
|
616
|
-
Heading({ level: 1, children: 'Title' }),
|
|
617
|
-
Heading({ level: 2, children: 'Sub' }),
|
|
618
|
-
],
|
|
599
|
+
children: [Heading({ level: 1, children: "Title" }), Heading({ level: 2, children: "Sub" })],
|
|
619
600
|
})
|
|
620
|
-
const text = (await render(doc,
|
|
621
|
-
expect(text).toContain(
|
|
622
|
-
expect(text).toContain(
|
|
623
|
-
expect(text).toContain(
|
|
624
|
-
expect(text).toContain(
|
|
601
|
+
const text = (await render(doc, "text")) as string
|
|
602
|
+
expect(text).toContain("TITLE")
|
|
603
|
+
expect(text).toContain("=====")
|
|
604
|
+
expect(text).toContain("Sub")
|
|
605
|
+
expect(text).toContain("---")
|
|
625
606
|
})
|
|
626
607
|
|
|
627
|
-
it(
|
|
608
|
+
it("renders aligned table columns", async () => {
|
|
628
609
|
const doc = Document({
|
|
629
610
|
children: Table({
|
|
630
611
|
columns: [
|
|
631
|
-
{ header:
|
|
632
|
-
{ header:
|
|
612
|
+
{ header: "Name", align: "left" },
|
|
613
|
+
{ header: "Price", align: "right" },
|
|
633
614
|
],
|
|
634
|
-
rows: [[
|
|
615
|
+
rows: [["Widget", "$10"]],
|
|
635
616
|
}),
|
|
636
617
|
})
|
|
637
|
-
const text = (await render(doc,
|
|
638
|
-
expect(text).toContain(
|
|
639
|
-
expect(text).toContain(
|
|
640
|
-
expect(text).toContain(
|
|
618
|
+
const text = (await render(doc, "text")) as string
|
|
619
|
+
expect(text).toContain("Name")
|
|
620
|
+
expect(text).toContain("Price")
|
|
621
|
+
expect(text).toContain("Widget")
|
|
641
622
|
})
|
|
642
623
|
|
|
643
|
-
it(
|
|
624
|
+
it("renders button as link reference", async () => {
|
|
644
625
|
const doc = Document({
|
|
645
|
-
children: Button({ href:
|
|
626
|
+
children: Button({ href: "/pay", children: "Pay" }),
|
|
646
627
|
})
|
|
647
|
-
const text = (await render(doc,
|
|
648
|
-
expect(text).toContain(
|
|
649
|
-
expect(text).toContain(
|
|
628
|
+
const text = (await render(doc, "text")) as string
|
|
629
|
+
expect(text).toContain("[Pay]")
|
|
630
|
+
expect(text).toContain("/pay")
|
|
650
631
|
})
|
|
651
632
|
|
|
652
|
-
it(
|
|
633
|
+
it("renders image as placeholder", async () => {
|
|
653
634
|
const doc = Document({
|
|
654
|
-
children: Image({ src:
|
|
635
|
+
children: Image({ src: "/x.png", alt: "Photo", caption: "Nice" }),
|
|
655
636
|
})
|
|
656
|
-
const text = (await render(doc,
|
|
657
|
-
expect(text).toContain(
|
|
637
|
+
const text = (await render(doc, "text")) as string
|
|
638
|
+
expect(text).toContain("[Photo — Nice]")
|
|
658
639
|
})
|
|
659
640
|
})
|
|
660
641
|
|
|
661
642
|
// ─── CSV Renderer ───────────────────────────────────────────────────────────
|
|
662
643
|
|
|
663
|
-
describe(
|
|
664
|
-
it(
|
|
644
|
+
describe("CSV renderer", () => {
|
|
645
|
+
it("extracts tables as CSV", async () => {
|
|
665
646
|
const doc = Document({
|
|
666
647
|
children: Table({
|
|
667
|
-
columns: [
|
|
648
|
+
columns: ["Name", "Price"],
|
|
668
649
|
rows: [
|
|
669
|
-
[
|
|
670
|
-
[
|
|
650
|
+
["Widget", "$10"],
|
|
651
|
+
["Gadget", "$20"],
|
|
671
652
|
],
|
|
672
653
|
}),
|
|
673
654
|
})
|
|
674
|
-
const csv = (await render(doc,
|
|
675
|
-
expect(csv).toContain(
|
|
676
|
-
expect(csv).toContain(
|
|
677
|
-
expect(csv).toContain(
|
|
655
|
+
const csv = (await render(doc, "csv")) as string
|
|
656
|
+
expect(csv).toContain("Name,Price")
|
|
657
|
+
expect(csv).toContain("Widget,$10")
|
|
658
|
+
expect(csv).toContain("Gadget,$20")
|
|
678
659
|
})
|
|
679
660
|
|
|
680
|
-
it(
|
|
661
|
+
it("escapes commas and quotes", async () => {
|
|
681
662
|
const doc = Document({
|
|
682
663
|
children: Table({
|
|
683
|
-
columns: [
|
|
684
|
-
rows: [[
|
|
664
|
+
columns: ["Name"],
|
|
665
|
+
rows: [["Widget, Inc."], ['He said "hello"']],
|
|
685
666
|
}),
|
|
686
667
|
})
|
|
687
|
-
const csv = (await render(doc,
|
|
668
|
+
const csv = (await render(doc, "csv")) as string
|
|
688
669
|
expect(csv).toContain('"Widget, Inc."')
|
|
689
670
|
expect(csv).toContain('"He said ""hello"""')
|
|
690
671
|
})
|
|
691
672
|
|
|
692
|
-
it(
|
|
693
|
-
const doc = Document({ children: Text({ children:
|
|
694
|
-
const csv = (await render(doc,
|
|
695
|
-
expect(csv).toContain(
|
|
673
|
+
it("returns message when no tables", async () => {
|
|
674
|
+
const doc = Document({ children: Text({ children: "no tables here" }) })
|
|
675
|
+
const csv = (await render(doc, "csv")) as string
|
|
676
|
+
expect(csv).toContain("No tables found")
|
|
696
677
|
})
|
|
697
678
|
|
|
698
|
-
it(
|
|
679
|
+
it("handles multiple tables", async () => {
|
|
699
680
|
const doc = Document({
|
|
700
681
|
children: [
|
|
701
|
-
Table({ columns: [
|
|
702
|
-
Table({ columns: [
|
|
682
|
+
Table({ columns: ["A"], rows: [["1"]] }),
|
|
683
|
+
Table({ columns: ["B"], rows: [["2"]] }),
|
|
703
684
|
],
|
|
704
685
|
})
|
|
705
|
-
const csv = (await render(doc,
|
|
706
|
-
expect(csv).toContain(
|
|
707
|
-
expect(csv).toContain(
|
|
686
|
+
const csv = (await render(doc, "csv")) as string
|
|
687
|
+
expect(csv).toContain("A")
|
|
688
|
+
expect(csv).toContain("B")
|
|
708
689
|
})
|
|
709
690
|
|
|
710
|
-
it(
|
|
691
|
+
it("adds caption as comment", async () => {
|
|
711
692
|
const doc = Document({
|
|
712
|
-
children: Table({ columns: [
|
|
693
|
+
children: Table({ columns: ["A"], rows: [["1"]], caption: "My Data" }),
|
|
713
694
|
})
|
|
714
|
-
const csv = (await render(doc,
|
|
715
|
-
expect(csv).toContain(
|
|
695
|
+
const csv = (await render(doc, "csv")) as string
|
|
696
|
+
expect(csv).toContain("# My Data")
|
|
716
697
|
})
|
|
717
698
|
})
|
|
718
699
|
|
|
719
700
|
// ─── Builder Pattern ────────────────────────────────────────────────────────
|
|
720
701
|
|
|
721
|
-
describe(
|
|
722
|
-
it(
|
|
723
|
-
const doc = createDocument({ title:
|
|
724
|
-
.heading('Title')
|
|
725
|
-
.text('Hello world')
|
|
702
|
+
describe("createDocument builder", () => {
|
|
703
|
+
it("builds a document with heading and text", async () => {
|
|
704
|
+
const doc = createDocument({ title: "Test" }).heading("Title").text("Hello world")
|
|
726
705
|
|
|
727
706
|
const node = doc.build()
|
|
728
|
-
expect(node.type).toBe(
|
|
729
|
-
expect(node.props.title).toBe(
|
|
707
|
+
expect(node.type).toBe("document")
|
|
708
|
+
expect(node.props.title).toBe("Test")
|
|
730
709
|
})
|
|
731
710
|
|
|
732
|
-
it(
|
|
711
|
+
it("renders to HTML", async () => {
|
|
733
712
|
const doc = createDocument()
|
|
734
|
-
.heading(
|
|
735
|
-
.text(
|
|
713
|
+
.heading("Report")
|
|
714
|
+
.text("Summary text")
|
|
736
715
|
.table({
|
|
737
|
-
columns: [
|
|
738
|
-
rows: [[
|
|
716
|
+
columns: ["Name", "Value"],
|
|
717
|
+
rows: [["A", "1"]],
|
|
739
718
|
})
|
|
740
719
|
|
|
741
720
|
const html = await doc.toHtml()
|
|
742
|
-
expect(html).toContain(
|
|
743
|
-
expect(html).toContain(
|
|
744
|
-
expect(html).toContain(
|
|
721
|
+
expect(html).toContain("Report")
|
|
722
|
+
expect(html).toContain("Summary text")
|
|
723
|
+
expect(html).toContain("<table")
|
|
745
724
|
})
|
|
746
725
|
|
|
747
|
-
it(
|
|
726
|
+
it("renders to markdown", async () => {
|
|
748
727
|
const doc = createDocument()
|
|
749
|
-
.heading(
|
|
750
|
-
.text(
|
|
751
|
-
.list([
|
|
728
|
+
.heading("Title")
|
|
729
|
+
.text("Body", { bold: true })
|
|
730
|
+
.list(["item 1", "item 2"])
|
|
752
731
|
|
|
753
732
|
const md = await doc.toMarkdown()
|
|
754
|
-
expect(md).toContain(
|
|
755
|
-
expect(md).toContain(
|
|
756
|
-
expect(md).toContain(
|
|
733
|
+
expect(md).toContain("# Title")
|
|
734
|
+
expect(md).toContain("**Body**")
|
|
735
|
+
expect(md).toContain("- item 1")
|
|
757
736
|
})
|
|
758
737
|
|
|
759
|
-
it(
|
|
760
|
-
const doc = createDocument().heading(
|
|
738
|
+
it("renders to text", async () => {
|
|
739
|
+
const doc = createDocument().heading("Title").text("Body")
|
|
761
740
|
|
|
762
741
|
const text = await doc.toText()
|
|
763
|
-
expect(text).toContain(
|
|
764
|
-
expect(text).toContain(
|
|
742
|
+
expect(text).toContain("TITLE")
|
|
743
|
+
expect(text).toContain("Body")
|
|
765
744
|
})
|
|
766
745
|
|
|
767
|
-
it(
|
|
768
|
-
const doc = createDocument().table({ columns: [
|
|
746
|
+
it("renders to CSV", async () => {
|
|
747
|
+
const doc = createDocument().table({ columns: ["X"], rows: [["1"], ["2"]] })
|
|
769
748
|
|
|
770
749
|
const csv = await doc.toCsv()
|
|
771
|
-
expect(csv).toContain(
|
|
772
|
-
expect(csv).toContain(
|
|
750
|
+
expect(csv).toContain("X")
|
|
751
|
+
expect(csv).toContain("1")
|
|
773
752
|
})
|
|
774
753
|
|
|
775
|
-
it(
|
|
754
|
+
it("supports all builder methods", () => {
|
|
776
755
|
const doc = createDocument()
|
|
777
|
-
.heading(
|
|
778
|
-
.text(
|
|
779
|
-
.paragraph(
|
|
780
|
-
.image(
|
|
781
|
-
.table({ columns: [
|
|
782
|
-
.list([
|
|
783
|
-
.code(
|
|
756
|
+
.heading("H")
|
|
757
|
+
.text("T")
|
|
758
|
+
.paragraph("P")
|
|
759
|
+
.image("/img.png")
|
|
760
|
+
.table({ columns: ["A"], rows: [["1"]] })
|
|
761
|
+
.list(["a", "b"])
|
|
762
|
+
.code("x = 1", { language: "python" })
|
|
784
763
|
.divider()
|
|
785
764
|
.spacer(20)
|
|
786
|
-
.quote(
|
|
787
|
-
.button(
|
|
788
|
-
.link(
|
|
765
|
+
.quote("Q")
|
|
766
|
+
.button("Click", { href: "/go" })
|
|
767
|
+
.link("Link", { href: "/link" })
|
|
789
768
|
|
|
790
769
|
const node = doc.build()
|
|
791
|
-
expect(node.type).toBe(
|
|
770
|
+
expect(node.type).toBe("document")
|
|
792
771
|
})
|
|
793
772
|
|
|
794
|
-
it(
|
|
773
|
+
it("chart without instance shows placeholder", async () => {
|
|
795
774
|
const doc = createDocument().chart(null)
|
|
796
775
|
|
|
797
776
|
const html = await doc.toHtml()
|
|
798
|
-
expect(html).toContain(
|
|
777
|
+
expect(html).toContain("[Chart]")
|
|
799
778
|
})
|
|
800
779
|
|
|
801
|
-
it(
|
|
780
|
+
it("flow without instance shows placeholder", async () => {
|
|
802
781
|
const doc = createDocument().flow(null)
|
|
803
782
|
|
|
804
783
|
const html = await doc.toHtml()
|
|
805
|
-
expect(html).toContain(
|
|
784
|
+
expect(html).toContain("[Flow Diagram]")
|
|
806
785
|
})
|
|
807
786
|
|
|
808
|
-
it(
|
|
787
|
+
it("chart with getDataURL captures image", async () => {
|
|
809
788
|
const mockChart = {
|
|
810
|
-
getDataURL: () =>
|
|
789
|
+
getDataURL: () => "data:image/png;base64,abc123",
|
|
811
790
|
}
|
|
812
791
|
const doc = createDocument().chart(mockChart, { width: 400 })
|
|
813
792
|
const html = await doc.toHtml()
|
|
814
|
-
expect(html).toContain(
|
|
793
|
+
expect(html).toContain("data:image/png;base64,abc123")
|
|
815
794
|
})
|
|
816
795
|
|
|
817
|
-
it(
|
|
796
|
+
it("flow with toSVG captures image", async () => {
|
|
818
797
|
const mockFlow = {
|
|
819
|
-
toSVG: () =>
|
|
798
|
+
toSVG: () => "<svg><rect/></svg>",
|
|
820
799
|
}
|
|
821
800
|
const doc = createDocument().flow(mockFlow, { width: 500 })
|
|
822
801
|
const html = await doc.toHtml()
|
|
823
|
-
expect(html).toContain(
|
|
802
|
+
expect(html).toContain("data:image/svg+xml")
|
|
824
803
|
})
|
|
825
804
|
})
|
|
826
805
|
|
|
827
806
|
// ─── Custom Renderers ───────────────────────────────────────────────────────
|
|
828
807
|
|
|
829
|
-
describe(
|
|
830
|
-
it(
|
|
831
|
-
registerRenderer(
|
|
808
|
+
describe("custom renderers", () => {
|
|
809
|
+
it("registerRenderer adds a custom format", async () => {
|
|
810
|
+
registerRenderer("custom", {
|
|
832
811
|
async render(node) {
|
|
833
812
|
return `CUSTOM:${node.type}`
|
|
834
813
|
},
|
|
835
814
|
})
|
|
836
815
|
|
|
837
|
-
const doc = Document({ children:
|
|
838
|
-
const result = await render(doc,
|
|
839
|
-
expect(result).toBe(
|
|
816
|
+
const doc = Document({ children: "hello" })
|
|
817
|
+
const result = await render(doc, "custom")
|
|
818
|
+
expect(result).toBe("CUSTOM:document")
|
|
840
819
|
})
|
|
841
820
|
|
|
842
|
-
it(
|
|
843
|
-
registerRenderer(
|
|
821
|
+
it("unregisterRenderer removes a format", () => {
|
|
822
|
+
registerRenderer("temp", {
|
|
844
823
|
async render() {
|
|
845
|
-
return
|
|
824
|
+
return "x"
|
|
846
825
|
},
|
|
847
826
|
})
|
|
848
|
-
unregisterRenderer(
|
|
849
|
-
expect(render(Document({ children:
|
|
850
|
-
'No renderer registered',
|
|
851
|
-
)
|
|
827
|
+
unregisterRenderer("temp")
|
|
828
|
+
expect(render(Document({ children: "x" }), "temp")).rejects.toThrow("No renderer registered")
|
|
852
829
|
})
|
|
853
830
|
|
|
854
|
-
it(
|
|
855
|
-
expect(render(Document({ children:
|
|
856
|
-
'No renderer registered',
|
|
857
|
-
)
|
|
831
|
+
it("throws for unknown format", () => {
|
|
832
|
+
expect(render(Document({ children: "x" }), "unknown")).rejects.toThrow("No renderer registered")
|
|
858
833
|
})
|
|
859
834
|
|
|
860
|
-
it(
|
|
835
|
+
it("lazy renderer is cached after first use", async () => {
|
|
861
836
|
let loadCount = 0
|
|
862
|
-
registerRenderer(
|
|
837
|
+
registerRenderer("lazy", async () => {
|
|
863
838
|
loadCount++
|
|
864
839
|
return {
|
|
865
840
|
async render() {
|
|
866
|
-
return
|
|
841
|
+
return "lazy-result"
|
|
867
842
|
},
|
|
868
843
|
}
|
|
869
844
|
})
|
|
870
845
|
|
|
871
|
-
await render(Document({ children:
|
|
872
|
-
await render(Document({ children:
|
|
846
|
+
await render(Document({ children: "x" }), "lazy")
|
|
847
|
+
await render(Document({ children: "x" }), "lazy")
|
|
873
848
|
expect(loadCount).toBe(1)
|
|
874
849
|
})
|
|
875
850
|
})
|
|
876
851
|
|
|
877
852
|
// ─── Real-World Document ────────────────────────────────────────────────────
|
|
878
853
|
|
|
879
|
-
describe(
|
|
854
|
+
describe("real-world document", () => {
|
|
880
855
|
function createInvoice() {
|
|
881
856
|
return Document({
|
|
882
|
-
title:
|
|
883
|
-
author:
|
|
857
|
+
title: "Invoice #1234",
|
|
858
|
+
author: "Acme Corp",
|
|
884
859
|
children: Page({
|
|
885
|
-
size:
|
|
860
|
+
size: "A4",
|
|
886
861
|
margin: 40,
|
|
887
862
|
children: [
|
|
888
863
|
Row({
|
|
889
864
|
gap: 20,
|
|
890
865
|
children: [
|
|
891
866
|
Column({
|
|
892
|
-
children: Image({ src:
|
|
867
|
+
children: Image({ src: "/logo.png", width: 80, alt: "Logo" }),
|
|
893
868
|
}),
|
|
894
869
|
Column({
|
|
895
870
|
children: [
|
|
896
|
-
Heading({ children:
|
|
897
|
-
Text({ color:
|
|
871
|
+
Heading({ children: "Invoice #1234" }),
|
|
872
|
+
Text({ color: "#666", children: "March 23, 2026" }),
|
|
898
873
|
],
|
|
899
874
|
}),
|
|
900
875
|
],
|
|
@@ -902,557 +877,544 @@ describe('real-world document', () => {
|
|
|
902
877
|
Spacer({ height: 30 }),
|
|
903
878
|
Table({
|
|
904
879
|
columns: [
|
|
905
|
-
{ header:
|
|
906
|
-
{ header:
|
|
907
|
-
{ header:
|
|
908
|
-
{ header:
|
|
880
|
+
{ header: "Item", width: "50%" },
|
|
881
|
+
{ header: "Qty", width: "15%", align: "center" },
|
|
882
|
+
{ header: "Price", width: "15%", align: "right" },
|
|
883
|
+
{ header: "Total", width: "20%", align: "right" },
|
|
909
884
|
],
|
|
910
885
|
rows: [
|
|
911
|
-
[
|
|
912
|
-
[
|
|
913
|
-
[
|
|
886
|
+
["Widget Pro", "2", "$50", "$100"],
|
|
887
|
+
["Gadget Plus", "1", "$75", "$75"],
|
|
888
|
+
["Service Fee", "1", "$25", "$25"],
|
|
914
889
|
],
|
|
915
890
|
striped: true,
|
|
916
|
-
headerStyle: { background:
|
|
891
|
+
headerStyle: { background: "#1a1a2e", color: "#fff" },
|
|
917
892
|
}),
|
|
918
893
|
Spacer({ height: 20 }),
|
|
919
894
|
Text({
|
|
920
895
|
bold: true,
|
|
921
|
-
align:
|
|
896
|
+
align: "right",
|
|
922
897
|
size: 18,
|
|
923
|
-
children:
|
|
898
|
+
children: "Total: $200",
|
|
924
899
|
}),
|
|
925
900
|
Divider(),
|
|
926
901
|
Text({
|
|
927
|
-
color:
|
|
902
|
+
color: "#999",
|
|
928
903
|
size: 12,
|
|
929
|
-
children:
|
|
904
|
+
children: "Thank you for your business!",
|
|
930
905
|
}),
|
|
931
906
|
Button({
|
|
932
|
-
href:
|
|
933
|
-
background:
|
|
934
|
-
align:
|
|
935
|
-
children:
|
|
907
|
+
href: "https://acme.com/pay/1234",
|
|
908
|
+
background: "#4f46e5",
|
|
909
|
+
align: "center",
|
|
910
|
+
children: "Pay Now",
|
|
936
911
|
}),
|
|
937
912
|
],
|
|
938
913
|
}),
|
|
939
914
|
})
|
|
940
915
|
}
|
|
941
916
|
|
|
942
|
-
it(
|
|
943
|
-
const html = (await render(createInvoice(),
|
|
944
|
-
expect(html).toContain(
|
|
945
|
-
expect(html).toContain(
|
|
946
|
-
expect(html).toContain(
|
|
947
|
-
expect(html).toContain(
|
|
917
|
+
it("renders as HTML", async () => {
|
|
918
|
+
const html = (await render(createInvoice(), "html")) as string
|
|
919
|
+
expect(html).toContain("Invoice #1234")
|
|
920
|
+
expect(html).toContain("Widget Pro")
|
|
921
|
+
expect(html).toContain("Total: $200")
|
|
922
|
+
expect(html).toContain("Pay Now")
|
|
948
923
|
})
|
|
949
924
|
|
|
950
|
-
it(
|
|
951
|
-
const html = (await render(createInvoice(),
|
|
952
|
-
expect(html).toContain(
|
|
953
|
-
expect(html).toContain(
|
|
954
|
-
expect(html).toContain(
|
|
925
|
+
it("renders as email", async () => {
|
|
926
|
+
const html = (await render(createInvoice(), "email")) as string
|
|
927
|
+
expect(html).toContain("Invoice #1234")
|
|
928
|
+
expect(html).toContain("max-width:600px")
|
|
929
|
+
expect(html).toContain("v:roundrect") // Outlook button
|
|
955
930
|
})
|
|
956
931
|
|
|
957
|
-
it(
|
|
958
|
-
const md = (await render(createInvoice(),
|
|
959
|
-
expect(md).toContain(
|
|
960
|
-
expect(md).toContain(
|
|
961
|
-
expect(md).toContain(
|
|
932
|
+
it("renders as markdown", async () => {
|
|
933
|
+
const md = (await render(createInvoice(), "md")) as string
|
|
934
|
+
expect(md).toContain("# Invoice #1234")
|
|
935
|
+
expect(md).toContain("| Widget Pro")
|
|
936
|
+
expect(md).toContain("**Total: $200**")
|
|
962
937
|
})
|
|
963
938
|
|
|
964
|
-
it(
|
|
965
|
-
const text = (await render(createInvoice(),
|
|
966
|
-
expect(text).toContain(
|
|
967
|
-
expect(text).toContain(
|
|
968
|
-
expect(text).toContain(
|
|
939
|
+
it("renders as text", async () => {
|
|
940
|
+
const text = (await render(createInvoice(), "text")) as string
|
|
941
|
+
expect(text).toContain("INVOICE #1234")
|
|
942
|
+
expect(text).toContain("Widget Pro")
|
|
943
|
+
expect(text).toContain("Total: $200")
|
|
969
944
|
})
|
|
970
945
|
|
|
971
|
-
it(
|
|
972
|
-
const csv = (await render(createInvoice(),
|
|
973
|
-
expect(csv).toContain(
|
|
974
|
-
expect(csv).toContain(
|
|
946
|
+
it("renders as CSV", async () => {
|
|
947
|
+
const csv = (await render(createInvoice(), "csv")) as string
|
|
948
|
+
expect(csv).toContain("Item,Qty,Price,Total")
|
|
949
|
+
expect(csv).toContain("Widget Pro,2,$50,$100")
|
|
975
950
|
})
|
|
976
951
|
})
|
|
977
952
|
|
|
978
953
|
// ─── Text Renderer — additional coverage ────────────────────────────────────
|
|
979
954
|
|
|
980
|
-
describe(
|
|
981
|
-
it(
|
|
955
|
+
describe("text renderer — additional", () => {
|
|
956
|
+
it("renders link with URL", async () => {
|
|
982
957
|
const doc = Document({
|
|
983
|
-
children: Link({ href:
|
|
958
|
+
children: Link({ href: "https://x.com", children: "Link" }),
|
|
984
959
|
})
|
|
985
|
-
const text = (await render(doc,
|
|
986
|
-
expect(text).toContain(
|
|
960
|
+
const text = (await render(doc, "text")) as string
|
|
961
|
+
expect(text).toContain("Link (https://x.com)")
|
|
987
962
|
})
|
|
988
963
|
|
|
989
|
-
it(
|
|
964
|
+
it("renders table with caption", async () => {
|
|
990
965
|
const doc = Document({
|
|
991
|
-
children: Table({ columns: [
|
|
966
|
+
children: Table({ columns: ["A"], rows: [["1"]], caption: "Data" }),
|
|
992
967
|
})
|
|
993
|
-
const text = (await render(doc,
|
|
994
|
-
expect(text).toContain(
|
|
968
|
+
const text = (await render(doc, "text")) as string
|
|
969
|
+
expect(text).toContain("Data")
|
|
995
970
|
})
|
|
996
971
|
|
|
997
|
-
it(
|
|
972
|
+
it("renders ordered list", async () => {
|
|
998
973
|
const doc = Document({
|
|
999
974
|
children: List({
|
|
1000
975
|
ordered: true,
|
|
1001
|
-
children: [
|
|
1002
|
-
ListItem({ children: 'one' }),
|
|
1003
|
-
ListItem({ children: 'two' }),
|
|
1004
|
-
],
|
|
976
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
1005
977
|
}),
|
|
1006
978
|
})
|
|
1007
|
-
const text = (await render(doc,
|
|
1008
|
-
expect(text).toContain(
|
|
1009
|
-
expect(text).toContain(
|
|
979
|
+
const text = (await render(doc, "text")) as string
|
|
980
|
+
expect(text).toContain("1. one")
|
|
981
|
+
expect(text).toContain("2. two")
|
|
1010
982
|
})
|
|
1011
983
|
|
|
1012
|
-
it(
|
|
984
|
+
it("renders unordered list", async () => {
|
|
1013
985
|
const doc = Document({
|
|
1014
986
|
children: List({
|
|
1015
|
-
children: [ListItem({ children:
|
|
987
|
+
children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
|
|
1016
988
|
}),
|
|
1017
989
|
})
|
|
1018
|
-
const text = (await render(doc,
|
|
1019
|
-
expect(text).toContain(
|
|
1020
|
-
expect(text).toContain(
|
|
990
|
+
const text = (await render(doc, "text")) as string
|
|
991
|
+
expect(text).toContain("* a")
|
|
992
|
+
expect(text).toContain("* b")
|
|
1021
993
|
})
|
|
1022
994
|
|
|
1023
|
-
it(
|
|
1024
|
-
const doc = Document({ children: Code({ children:
|
|
1025
|
-
const text = (await render(doc,
|
|
1026
|
-
expect(text).toContain(
|
|
995
|
+
it("renders code block", async () => {
|
|
996
|
+
const doc = Document({ children: Code({ children: "x = 1" }) })
|
|
997
|
+
const text = (await render(doc, "text")) as string
|
|
998
|
+
expect(text).toContain("x = 1")
|
|
1027
999
|
})
|
|
1028
1000
|
|
|
1029
|
-
it(
|
|
1001
|
+
it("renders divider", async () => {
|
|
1030
1002
|
const doc = Document({ children: Divider() })
|
|
1031
|
-
const text = (await render(doc,
|
|
1032
|
-
expect(text).toContain(
|
|
1003
|
+
const text = (await render(doc, "text")) as string
|
|
1004
|
+
expect(text).toContain("─")
|
|
1033
1005
|
})
|
|
1034
1006
|
|
|
1035
|
-
it(
|
|
1007
|
+
it("renders spacer as newline", async () => {
|
|
1036
1008
|
const doc = Document({
|
|
1037
|
-
children: [
|
|
1038
|
-
Text({ children: 'A' }),
|
|
1039
|
-
Spacer({ height: 20 }),
|
|
1040
|
-
Text({ children: 'B' }),
|
|
1041
|
-
],
|
|
1009
|
+
children: [Text({ children: "A" }), Spacer({ height: 20 }), Text({ children: "B" })],
|
|
1042
1010
|
})
|
|
1043
|
-
const text = (await render(doc,
|
|
1044
|
-
expect(text).toContain(
|
|
1045
|
-
expect(text).toContain(
|
|
1011
|
+
const text = (await render(doc, "text")) as string
|
|
1012
|
+
expect(text).toContain("A")
|
|
1013
|
+
expect(text).toContain("B")
|
|
1046
1014
|
})
|
|
1047
1015
|
|
|
1048
|
-
it(
|
|
1049
|
-
const doc = Document({ children: Quote({ children:
|
|
1050
|
-
const text = (await render(doc,
|
|
1016
|
+
it("renders quote with indentation", async () => {
|
|
1017
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
1018
|
+
const text = (await render(doc, "text")) as string
|
|
1051
1019
|
expect(text).toContain('"wise"')
|
|
1052
1020
|
})
|
|
1053
1021
|
|
|
1054
|
-
it(
|
|
1022
|
+
it("renders section/row/column", async () => {
|
|
1055
1023
|
const doc = Document({
|
|
1056
1024
|
children: Section({
|
|
1057
1025
|
children: Row({
|
|
1058
|
-
children: Column({ children: Text({ children:
|
|
1026
|
+
children: Column({ children: Text({ children: "nested" }) }),
|
|
1059
1027
|
}),
|
|
1060
1028
|
}),
|
|
1061
1029
|
})
|
|
1062
|
-
const text = (await render(doc,
|
|
1063
|
-
expect(text).toContain(
|
|
1030
|
+
const text = (await render(doc, "text")) as string
|
|
1031
|
+
expect(text).toContain("nested")
|
|
1064
1032
|
})
|
|
1065
1033
|
|
|
1066
|
-
it(
|
|
1067
|
-
const doc = Document({ children: Heading({ level: 4, children:
|
|
1068
|
-
const text = (await render(doc,
|
|
1069
|
-
expect(text).toContain(
|
|
1034
|
+
it("renders heading level 3+", async () => {
|
|
1035
|
+
const doc = Document({ children: Heading({ level: 4, children: "Sub" }) })
|
|
1036
|
+
const text = (await render(doc, "text")) as string
|
|
1037
|
+
expect(text).toContain("Sub")
|
|
1070
1038
|
// Level 3+ should not have underline
|
|
1071
|
-
expect(text).not.toContain(
|
|
1072
|
-
expect(text).not.toContain(
|
|
1039
|
+
expect(text).not.toContain("===")
|
|
1040
|
+
expect(text).not.toContain("---")
|
|
1073
1041
|
})
|
|
1074
1042
|
|
|
1075
|
-
it(
|
|
1043
|
+
it("renders table with center aligned column", async () => {
|
|
1076
1044
|
const doc = Document({
|
|
1077
1045
|
children: Table({
|
|
1078
|
-
columns: [{ header:
|
|
1079
|
-
rows: [[
|
|
1046
|
+
columns: [{ header: "Name", align: "center" }],
|
|
1047
|
+
rows: [["X"]],
|
|
1080
1048
|
}),
|
|
1081
1049
|
})
|
|
1082
|
-
const text = (await render(doc,
|
|
1083
|
-
expect(text).toContain(
|
|
1084
|
-
expect(text).toContain(
|
|
1050
|
+
const text = (await render(doc, "text")) as string
|
|
1051
|
+
expect(text).toContain("Name")
|
|
1052
|
+
expect(text).toContain("X")
|
|
1085
1053
|
})
|
|
1086
1054
|
})
|
|
1087
1055
|
|
|
1088
1056
|
// ─── Builder — additional coverage ───────────────────────────────────────────
|
|
1089
1057
|
|
|
1090
|
-
describe(
|
|
1091
|
-
it(
|
|
1092
|
-
const doc = createDocument().heading(
|
|
1058
|
+
describe("builder — additional", () => {
|
|
1059
|
+
it("pageBreak wraps content", () => {
|
|
1060
|
+
const doc = createDocument().heading("Page 1").pageBreak().heading("Page 2")
|
|
1093
1061
|
const node = doc.build()
|
|
1094
|
-
expect(node.type).toBe(
|
|
1062
|
+
expect(node.type).toBe("document")
|
|
1095
1063
|
})
|
|
1096
1064
|
|
|
1097
|
-
it(
|
|
1098
|
-
const html = await createDocument().heading(
|
|
1099
|
-
expect(html).toContain(
|
|
1100
|
-
expect(html).toContain(
|
|
1065
|
+
it("toEmail renders", async () => {
|
|
1066
|
+
const html = await createDocument().heading("Hi").toEmail()
|
|
1067
|
+
expect(html).toContain("Hi")
|
|
1068
|
+
expect(html).toContain("max-width:600px")
|
|
1101
1069
|
})
|
|
1102
1070
|
|
|
1103
|
-
it(
|
|
1071
|
+
it("toCsv renders", async () => {
|
|
1104
1072
|
const csv = await createDocument()
|
|
1105
|
-
.table({ columns: [
|
|
1073
|
+
.table({ columns: ["X"], rows: [["1"]] })
|
|
1106
1074
|
.toCsv()
|
|
1107
|
-
expect(csv).toContain(
|
|
1075
|
+
expect(csv).toContain("X")
|
|
1108
1076
|
})
|
|
1109
1077
|
|
|
1110
|
-
it(
|
|
1111
|
-
const text = await createDocument().heading(
|
|
1112
|
-
expect(text).toContain(
|
|
1078
|
+
it("toText renders", async () => {
|
|
1079
|
+
const text = await createDocument().heading("Hi").toText()
|
|
1080
|
+
expect(text).toContain("HI")
|
|
1113
1081
|
})
|
|
1114
1082
|
})
|
|
1115
1083
|
|
|
1116
1084
|
// ─── Markdown — additional branch coverage ──────────────────────────────────
|
|
1117
1085
|
|
|
1118
|
-
describe(
|
|
1119
|
-
it(
|
|
1086
|
+
describe("markdown — additional branches", () => {
|
|
1087
|
+
it("renders table with center aligned column", async () => {
|
|
1120
1088
|
const doc = Document({
|
|
1121
1089
|
children: Table({
|
|
1122
|
-
columns: [{ header:
|
|
1123
|
-
rows: [[
|
|
1090
|
+
columns: [{ header: "X", align: "center" }],
|
|
1091
|
+
rows: [["1"]],
|
|
1124
1092
|
}),
|
|
1125
1093
|
})
|
|
1126
|
-
const md = (await render(doc,
|
|
1127
|
-
expect(md).toContain(
|
|
1094
|
+
const md = (await render(doc, "md")) as string
|
|
1095
|
+
expect(md).toContain(":---:")
|
|
1128
1096
|
})
|
|
1129
1097
|
|
|
1130
|
-
it(
|
|
1098
|
+
it("renders table with left aligned column (default)", async () => {
|
|
1131
1099
|
const doc = Document({
|
|
1132
1100
|
children: Table({
|
|
1133
|
-
columns: [{ header:
|
|
1134
|
-
rows: [[
|
|
1101
|
+
columns: [{ header: "X", align: "left" }],
|
|
1102
|
+
rows: [["1"]],
|
|
1135
1103
|
}),
|
|
1136
1104
|
})
|
|
1137
|
-
const md = (await render(doc,
|
|
1138
|
-
expect(md).toContain(
|
|
1105
|
+
const md = (await render(doc, "md")) as string
|
|
1106
|
+
expect(md).toContain("| --- |")
|
|
1139
1107
|
})
|
|
1140
1108
|
|
|
1141
|
-
it(
|
|
1109
|
+
it("renders empty table gracefully", async () => {
|
|
1142
1110
|
const doc = Document({
|
|
1143
1111
|
children: Table({ columns: [], rows: [] }),
|
|
1144
1112
|
})
|
|
1145
|
-
const md = (await render(doc,
|
|
1113
|
+
const md = (await render(doc, "md")) as string
|
|
1146
1114
|
expect(md).toBeDefined()
|
|
1147
1115
|
})
|
|
1148
1116
|
|
|
1149
|
-
it(
|
|
1150
|
-
const doc = Document({ children: Image({ src:
|
|
1151
|
-
const md = (await render(doc,
|
|
1152
|
-
expect(md).toContain(
|
|
1153
|
-
expect(md).not.toContain(
|
|
1117
|
+
it("renders image without caption", async () => {
|
|
1118
|
+
const doc = Document({ children: Image({ src: "/x.png", alt: "X" }) })
|
|
1119
|
+
const md = (await render(doc, "md")) as string
|
|
1120
|
+
expect(md).toContain("")
|
|
1121
|
+
expect(md).not.toContain("*")
|
|
1154
1122
|
})
|
|
1155
1123
|
|
|
1156
|
-
it(
|
|
1157
|
-
const doc = Document({ children: Image({ src:
|
|
1158
|
-
const md = (await render(doc,
|
|
1159
|
-
expect(md).toContain(
|
|
1124
|
+
it("renders image without alt", async () => {
|
|
1125
|
+
const doc = Document({ children: Image({ src: "/x.png" }) })
|
|
1126
|
+
const md = (await render(doc, "md")) as string
|
|
1127
|
+
expect(md).toContain("")
|
|
1160
1128
|
})
|
|
1161
1129
|
|
|
1162
|
-
it(
|
|
1163
|
-
const doc = Document({ children: Code({ children:
|
|
1164
|
-
const md = (await render(doc,
|
|
1165
|
-
expect(md).toContain(
|
|
1130
|
+
it("renders code without language", async () => {
|
|
1131
|
+
const doc = Document({ children: Code({ children: "x = 1" }) })
|
|
1132
|
+
const md = (await render(doc, "md")) as string
|
|
1133
|
+
expect(md).toContain("```\nx = 1\n```")
|
|
1166
1134
|
})
|
|
1167
1135
|
|
|
1168
|
-
it(
|
|
1136
|
+
it("renders spacer as newline", async () => {
|
|
1169
1137
|
const doc = Document({ children: Spacer({ height: 20 }) })
|
|
1170
|
-
const md = (await render(doc,
|
|
1138
|
+
const md = (await render(doc, "md")) as string
|
|
1171
1139
|
expect(md).toBeDefined()
|
|
1172
1140
|
})
|
|
1173
1141
|
})
|
|
1174
1142
|
|
|
1175
1143
|
// ─── Email — additional branch coverage ─────────────────────────────────────
|
|
1176
1144
|
|
|
1177
|
-
describe(
|
|
1178
|
-
it(
|
|
1145
|
+
describe("email — additional branches", () => {
|
|
1146
|
+
it("renders section with background and padding", async () => {
|
|
1179
1147
|
const doc = Document({
|
|
1180
1148
|
children: Section({
|
|
1181
|
-
background:
|
|
1149
|
+
background: "#f00",
|
|
1182
1150
|
padding: [10, 20],
|
|
1183
1151
|
borderRadius: 8,
|
|
1184
|
-
children: Text({ children:
|
|
1152
|
+
children: Text({ children: "hi" }),
|
|
1185
1153
|
}),
|
|
1186
1154
|
})
|
|
1187
|
-
const html = (await render(doc,
|
|
1188
|
-
expect(html).toContain(
|
|
1189
|
-
expect(html).toContain(
|
|
1155
|
+
const html = (await render(doc, "email")) as string
|
|
1156
|
+
expect(html).toContain("background-color:#f00")
|
|
1157
|
+
expect(html).toContain("border-radius:8px")
|
|
1190
1158
|
})
|
|
1191
1159
|
|
|
1192
|
-
it(
|
|
1193
|
-
const doc = Document({ children: Image({ src:
|
|
1194
|
-
const html = (await render(doc,
|
|
1195
|
-
expect(html).toContain(
|
|
1160
|
+
it("renders image with right alignment", async () => {
|
|
1161
|
+
const doc = Document({ children: Image({ src: "/x.png", align: "right" }) })
|
|
1162
|
+
const html = (await render(doc, "email")) as string
|
|
1163
|
+
expect(html).toContain("text-align:right")
|
|
1196
1164
|
})
|
|
1197
1165
|
|
|
1198
|
-
it(
|
|
1166
|
+
it("renders striped table", async () => {
|
|
1199
1167
|
const doc = Document({
|
|
1200
1168
|
children: Table({
|
|
1201
|
-
columns: [
|
|
1202
|
-
rows: [[
|
|
1169
|
+
columns: ["A"],
|
|
1170
|
+
rows: [["1"], ["2"], ["3"]],
|
|
1203
1171
|
striped: true,
|
|
1204
1172
|
}),
|
|
1205
1173
|
})
|
|
1206
|
-
const html = (await render(doc,
|
|
1207
|
-
expect(html).toContain(
|
|
1174
|
+
const html = (await render(doc, "email")) as string
|
|
1175
|
+
expect(html).toContain("background-color:#f9f9f9")
|
|
1208
1176
|
})
|
|
1209
1177
|
|
|
1210
|
-
it(
|
|
1211
|
-
const doc = Document({ children: Heading({ level: 2, children:
|
|
1212
|
-
const html = (await render(doc,
|
|
1213
|
-
expect(html).toContain(
|
|
1214
|
-
expect(html).toContain(
|
|
1178
|
+
it("renders heading level 2", async () => {
|
|
1179
|
+
const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
|
|
1180
|
+
const html = (await render(doc, "email")) as string
|
|
1181
|
+
expect(html).toContain("<h2")
|
|
1182
|
+
expect(html).toContain("font-size:24px")
|
|
1215
1183
|
})
|
|
1216
1184
|
|
|
1217
|
-
it(
|
|
1185
|
+
it("renders text with all formatting options", async () => {
|
|
1218
1186
|
const doc = Document({
|
|
1219
1187
|
children: Text({
|
|
1220
1188
|
size: 16,
|
|
1221
1189
|
bold: true,
|
|
1222
1190
|
italic: true,
|
|
1223
1191
|
underline: true,
|
|
1224
|
-
align:
|
|
1192
|
+
align: "center",
|
|
1225
1193
|
lineHeight: 2,
|
|
1226
|
-
children:
|
|
1194
|
+
children: "styled",
|
|
1227
1195
|
}),
|
|
1228
1196
|
})
|
|
1229
|
-
const html = (await render(doc,
|
|
1230
|
-
expect(html).toContain(
|
|
1231
|
-
expect(html).toContain(
|
|
1232
|
-
expect(html).toContain(
|
|
1233
|
-
expect(html).toContain(
|
|
1234
|
-
expect(html).toContain(
|
|
1197
|
+
const html = (await render(doc, "email")) as string
|
|
1198
|
+
expect(html).toContain("font-size:16px")
|
|
1199
|
+
expect(html).toContain("font-weight:bold")
|
|
1200
|
+
expect(html).toContain("font-style:italic")
|
|
1201
|
+
expect(html).toContain("text-decoration:underline")
|
|
1202
|
+
expect(html).toContain("text-align:center")
|
|
1235
1203
|
})
|
|
1236
1204
|
|
|
1237
|
-
it(
|
|
1205
|
+
it("renders text with strikethrough", async () => {
|
|
1238
1206
|
const doc = Document({
|
|
1239
|
-
children: Text({ strikethrough: true, children:
|
|
1207
|
+
children: Text({ strikethrough: true, children: "old" }),
|
|
1240
1208
|
})
|
|
1241
|
-
const html = (await render(doc,
|
|
1242
|
-
expect(html).toContain(
|
|
1209
|
+
const html = (await render(doc, "email")) as string
|
|
1210
|
+
expect(html).toContain("text-decoration:line-through")
|
|
1243
1211
|
})
|
|
1244
1212
|
|
|
1245
|
-
it(
|
|
1213
|
+
it("renders button with custom alignment", async () => {
|
|
1246
1214
|
const doc = Document({
|
|
1247
|
-
children: Button({ href:
|
|
1215
|
+
children: Button({ href: "/x", align: "center", children: "Go" }),
|
|
1248
1216
|
})
|
|
1249
|
-
const html = (await render(doc,
|
|
1250
|
-
expect(html).toContain(
|
|
1217
|
+
const html = (await render(doc, "email")) as string
|
|
1218
|
+
expect(html).toContain("text-align:center")
|
|
1251
1219
|
})
|
|
1252
1220
|
|
|
1253
|
-
it(
|
|
1221
|
+
it("renders section column direction (default)", async () => {
|
|
1254
1222
|
const doc = Document({
|
|
1255
|
-
children: Section({ children: Text({ children:
|
|
1223
|
+
children: Section({ children: Text({ children: "content" }) }),
|
|
1256
1224
|
})
|
|
1257
|
-
const html = (await render(doc,
|
|
1258
|
-
expect(html).toContain(
|
|
1225
|
+
const html = (await render(doc, "email")) as string
|
|
1226
|
+
expect(html).toContain("content")
|
|
1259
1227
|
})
|
|
1260
1228
|
|
|
1261
|
-
it(
|
|
1229
|
+
it("renders section with gap in row", async () => {
|
|
1262
1230
|
const doc = Document({
|
|
1263
1231
|
children: Section({
|
|
1264
|
-
direction:
|
|
1232
|
+
direction: "row",
|
|
1265
1233
|
gap: 16,
|
|
1266
|
-
children: [Text({ children:
|
|
1234
|
+
children: [Text({ children: "a" }), Text({ children: "b" })],
|
|
1267
1235
|
}),
|
|
1268
1236
|
})
|
|
1269
|
-
const html = (await render(doc,
|
|
1270
|
-
expect(html).toContain(
|
|
1237
|
+
const html = (await render(doc, "email")) as string
|
|
1238
|
+
expect(html).toContain("padding:0 8px")
|
|
1271
1239
|
})
|
|
1272
1240
|
})
|
|
1273
1241
|
|
|
1274
1242
|
// ─── HTML — additional branch coverage ──────────────────────────────────────
|
|
1275
1243
|
|
|
1276
|
-
describe(
|
|
1277
|
-
it(
|
|
1244
|
+
describe("html — additional branches", () => {
|
|
1245
|
+
it("renders section column direction (default)", async () => {
|
|
1278
1246
|
const doc = Document({
|
|
1279
|
-
children: Section({ children: Text({ children:
|
|
1247
|
+
children: Section({ children: Text({ children: "x" }) }),
|
|
1280
1248
|
})
|
|
1281
|
-
const html = (await render(doc,
|
|
1282
|
-
expect(html).not.toContain(
|
|
1249
|
+
const html = (await render(doc, "html")) as string
|
|
1250
|
+
expect(html).not.toContain("display:flex")
|
|
1283
1251
|
})
|
|
1284
1252
|
|
|
1285
|
-
it(
|
|
1253
|
+
it("renders page with margin as array", async () => {
|
|
1286
1254
|
const doc = Document({
|
|
1287
|
-
children: Page({ margin: [10, 20], children: Text({ children:
|
|
1255
|
+
children: Page({ margin: [10, 20], children: Text({ children: "hi" }) }),
|
|
1288
1256
|
})
|
|
1289
|
-
const html = (await render(doc,
|
|
1290
|
-
expect(html).toContain(
|
|
1257
|
+
const html = (await render(doc, "html")) as string
|
|
1258
|
+
expect(html).toContain("10px 20px")
|
|
1291
1259
|
})
|
|
1292
1260
|
|
|
1293
|
-
it(
|
|
1261
|
+
it("renders page with 4-value margin", async () => {
|
|
1294
1262
|
const doc = Document({
|
|
1295
1263
|
children: Page({
|
|
1296
1264
|
margin: [10, 20, 30, 40],
|
|
1297
|
-
children: Text({ children:
|
|
1265
|
+
children: Text({ children: "hi" }),
|
|
1298
1266
|
}),
|
|
1299
1267
|
})
|
|
1300
|
-
const html = (await render(doc,
|
|
1301
|
-
expect(html).toContain(
|
|
1268
|
+
const html = (await render(doc, "html")) as string
|
|
1269
|
+
expect(html).toContain("10px 20px 30px 40px")
|
|
1302
1270
|
})
|
|
1303
1271
|
|
|
1304
|
-
it(
|
|
1272
|
+
it("renders text with lineHeight", async () => {
|
|
1305
1273
|
const doc = Document({
|
|
1306
|
-
children: Text({ lineHeight: 1.8, children:
|
|
1274
|
+
children: Text({ lineHeight: 1.8, children: "text" }),
|
|
1307
1275
|
})
|
|
1308
|
-
const html = (await render(doc,
|
|
1309
|
-
expect(html).toContain(
|
|
1276
|
+
const html = (await render(doc, "html")) as string
|
|
1277
|
+
expect(html).toContain("line-height:1.8")
|
|
1310
1278
|
})
|
|
1311
1279
|
})
|
|
1312
1280
|
|
|
1313
1281
|
// ─── CSV — additional branch coverage ───────────────────────────────────────
|
|
1314
1282
|
|
|
1315
|
-
describe(
|
|
1316
|
-
it(
|
|
1283
|
+
describe("csv — additional branches", () => {
|
|
1284
|
+
it("finds tables nested in pages", async () => {
|
|
1317
1285
|
const doc = Document({
|
|
1318
1286
|
children: Page({
|
|
1319
1287
|
children: Section({
|
|
1320
|
-
children: Table({ columns: [
|
|
1288
|
+
children: Table({ columns: ["Nested"], rows: [["val"]] }),
|
|
1321
1289
|
}),
|
|
1322
1290
|
}),
|
|
1323
1291
|
})
|
|
1324
|
-
const csv = (await render(doc,
|
|
1325
|
-
expect(csv).toContain(
|
|
1326
|
-
expect(csv).toContain(
|
|
1292
|
+
const csv = (await render(doc, "csv")) as string
|
|
1293
|
+
expect(csv).toContain("Nested")
|
|
1294
|
+
expect(csv).toContain("val")
|
|
1327
1295
|
})
|
|
1328
1296
|
})
|
|
1329
1297
|
|
|
1330
1298
|
// ─── Render Dispatcher — additional coverage ────────────────────────────────
|
|
1331
1299
|
|
|
1332
|
-
describe(
|
|
1333
|
-
it(
|
|
1300
|
+
describe("render dispatcher — additional", () => {
|
|
1301
|
+
it("error message includes available formats", async () => {
|
|
1334
1302
|
try {
|
|
1335
|
-
await render(Document({ children:
|
|
1303
|
+
await render(Document({ children: "x" }), "nonexistent")
|
|
1336
1304
|
} catch (e) {
|
|
1337
|
-
expect((e as Error).message).toContain(
|
|
1338
|
-
expect((e as Error).message).toContain(
|
|
1339
|
-
expect((e as Error).message).toContain(
|
|
1305
|
+
expect((e as Error).message).toContain("No renderer registered")
|
|
1306
|
+
expect((e as Error).message).toContain("Available:")
|
|
1307
|
+
expect((e as Error).message).toContain("html")
|
|
1340
1308
|
}
|
|
1341
1309
|
})
|
|
1342
1310
|
})
|
|
1343
1311
|
|
|
1344
1312
|
// ─── Email Renderer — additional coverage ───────────────────────────────────
|
|
1345
1313
|
|
|
1346
|
-
describe(
|
|
1347
|
-
it(
|
|
1314
|
+
describe("email renderer — additional", () => {
|
|
1315
|
+
it("renders image with caption", async () => {
|
|
1348
1316
|
const doc = Document({
|
|
1349
|
-
children: Image({ src:
|
|
1317
|
+
children: Image({ src: "/x.png", alt: "Photo", caption: "Nice" }),
|
|
1350
1318
|
})
|
|
1351
|
-
const html = (await render(doc,
|
|
1352
|
-
expect(html).toContain(
|
|
1319
|
+
const html = (await render(doc, "email")) as string
|
|
1320
|
+
expect(html).toContain("Nice")
|
|
1353
1321
|
})
|
|
1354
1322
|
|
|
1355
|
-
it(
|
|
1323
|
+
it("renders image with center alignment", async () => {
|
|
1356
1324
|
const doc = Document({
|
|
1357
|
-
children: Image({ src:
|
|
1325
|
+
children: Image({ src: "/x.png", align: "center" }),
|
|
1358
1326
|
})
|
|
1359
|
-
const html = (await render(doc,
|
|
1360
|
-
expect(html).toContain(
|
|
1327
|
+
const html = (await render(doc, "email")) as string
|
|
1328
|
+
expect(html).toContain("text-align:center")
|
|
1361
1329
|
})
|
|
1362
1330
|
|
|
1363
|
-
it(
|
|
1364
|
-
const doc = Document({ children: Code({ children:
|
|
1365
|
-
const html = (await render(doc,
|
|
1366
|
-
expect(html).toContain(
|
|
1367
|
-
expect(html).toContain(
|
|
1331
|
+
it("renders code block", async () => {
|
|
1332
|
+
const doc = Document({ children: Code({ children: "const x = 1" }) })
|
|
1333
|
+
const html = (await render(doc, "email")) as string
|
|
1334
|
+
expect(html).toContain("Courier New")
|
|
1335
|
+
expect(html).toContain("const x = 1")
|
|
1368
1336
|
})
|
|
1369
1337
|
|
|
1370
|
-
it(
|
|
1338
|
+
it("renders spacer with line-height trick", async () => {
|
|
1371
1339
|
const doc = Document({ children: Spacer({ height: 20 }) })
|
|
1372
|
-
const html = (await render(doc,
|
|
1373
|
-
expect(html).toContain(
|
|
1374
|
-
expect(html).toContain(
|
|
1340
|
+
const html = (await render(doc, "email")) as string
|
|
1341
|
+
expect(html).toContain("height:20px")
|
|
1342
|
+
expect(html).toContain("line-height:20px")
|
|
1375
1343
|
})
|
|
1376
1344
|
|
|
1377
|
-
it(
|
|
1345
|
+
it("renders list", async () => {
|
|
1378
1346
|
const doc = Document({
|
|
1379
1347
|
children: List({
|
|
1380
|
-
children: [
|
|
1381
|
-
ListItem({ children: 'one' }),
|
|
1382
|
-
ListItem({ children: 'two' }),
|
|
1383
|
-
],
|
|
1348
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
1384
1349
|
}),
|
|
1385
1350
|
})
|
|
1386
|
-
const html = (await render(doc,
|
|
1387
|
-
expect(html).toContain(
|
|
1388
|
-
expect(html).toContain(
|
|
1351
|
+
const html = (await render(doc, "email")) as string
|
|
1352
|
+
expect(html).toContain("<ul")
|
|
1353
|
+
expect(html).toContain("<li")
|
|
1389
1354
|
})
|
|
1390
1355
|
|
|
1391
|
-
it(
|
|
1356
|
+
it("renders table caption", async () => {
|
|
1392
1357
|
const doc = Document({
|
|
1393
|
-
children: Table({ columns: [
|
|
1358
|
+
children: Table({ columns: ["A"], rows: [["1"]], caption: "Data" }),
|
|
1394
1359
|
})
|
|
1395
|
-
const html = (await render(doc,
|
|
1396
|
-
expect(html).toContain(
|
|
1360
|
+
const html = (await render(doc, "email")) as string
|
|
1361
|
+
expect(html).toContain("Data")
|
|
1397
1362
|
})
|
|
1398
1363
|
|
|
1399
|
-
it(
|
|
1364
|
+
it("renders link with target _blank", async () => {
|
|
1400
1365
|
const doc = Document({
|
|
1401
|
-
children: Link({ href:
|
|
1366
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
1402
1367
|
})
|
|
1403
|
-
const html = (await render(doc,
|
|
1368
|
+
const html = (await render(doc, "email")) as string
|
|
1404
1369
|
expect(html).toContain('target="_blank"')
|
|
1405
1370
|
})
|
|
1406
1371
|
|
|
1407
|
-
it(
|
|
1372
|
+
it("renders row layout using tables", async () => {
|
|
1408
1373
|
const doc = Document({
|
|
1409
1374
|
children: Row({
|
|
1410
1375
|
gap: 10,
|
|
1411
|
-
children: [Text({ children:
|
|
1376
|
+
children: [Text({ children: "L" }), Text({ children: "R" })],
|
|
1412
1377
|
}),
|
|
1413
1378
|
})
|
|
1414
|
-
const html = (await render(doc,
|
|
1415
|
-
expect(html).toContain(
|
|
1379
|
+
const html = (await render(doc, "email")) as string
|
|
1380
|
+
expect(html).toContain("<table")
|
|
1416
1381
|
expect(html).toContain('valign="top"')
|
|
1417
1382
|
})
|
|
1418
1383
|
})
|
|
1419
1384
|
|
|
1420
1385
|
// ─── DOCX Renderer (integration) ────────────────────────────────────────────
|
|
1421
1386
|
|
|
1422
|
-
describe(
|
|
1423
|
-
it(
|
|
1387
|
+
describe("DOCX renderer", () => {
|
|
1388
|
+
it("renders a document with heading, text, table, list, code, divider to a valid Uint8Array", async () => {
|
|
1424
1389
|
const doc = Document({
|
|
1425
|
-
title:
|
|
1426
|
-
author:
|
|
1390
|
+
title: "DOCX Test",
|
|
1391
|
+
author: "Test Suite",
|
|
1427
1392
|
children: Page({
|
|
1428
|
-
size:
|
|
1393
|
+
size: "A4",
|
|
1429
1394
|
margin: 40,
|
|
1430
1395
|
children: [
|
|
1431
|
-
Heading({ children:
|
|
1432
|
-
Text({ children:
|
|
1396
|
+
Heading({ children: "DOCX Integration Test" }),
|
|
1397
|
+
Text({ children: "A test paragraph.", bold: true }),
|
|
1433
1398
|
Table({
|
|
1434
|
-
columns: [
|
|
1399
|
+
columns: ["Name", "Value"],
|
|
1435
1400
|
rows: [
|
|
1436
|
-
[
|
|
1437
|
-
[
|
|
1401
|
+
["Alpha", "100"],
|
|
1402
|
+
["Beta", "200"],
|
|
1438
1403
|
],
|
|
1439
1404
|
striped: true,
|
|
1440
|
-
headerStyle: { background:
|
|
1405
|
+
headerStyle: { background: "#333333", color: "#ffffff" },
|
|
1441
1406
|
}),
|
|
1442
1407
|
List({
|
|
1443
1408
|
ordered: true,
|
|
1444
|
-
children: [
|
|
1445
|
-
ListItem({ children: 'First' }),
|
|
1446
|
-
ListItem({ children: 'Second' }),
|
|
1447
|
-
],
|
|
1409
|
+
children: [ListItem({ children: "First" }), ListItem({ children: "Second" })],
|
|
1448
1410
|
}),
|
|
1449
|
-
Code({ children:
|
|
1411
|
+
Code({ children: "const x = 42" }),
|
|
1450
1412
|
Divider(),
|
|
1451
1413
|
],
|
|
1452
1414
|
}),
|
|
1453
1415
|
})
|
|
1454
1416
|
|
|
1455
|
-
const result = await render(doc,
|
|
1417
|
+
const result = await render(doc, "docx")
|
|
1456
1418
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1457
1419
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1458
1420
|
// DOCX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
|
|
@@ -1460,81 +1422,76 @@ describe('DOCX renderer', () => {
|
|
|
1460
1422
|
expect((result as Uint8Array)[1]).toBe(0x4b)
|
|
1461
1423
|
}, 15000)
|
|
1462
1424
|
|
|
1463
|
-
it(
|
|
1425
|
+
it("embeds base64 images via ImageRun", async () => {
|
|
1464
1426
|
// 1x1 red pixel PNG as base64
|
|
1465
1427
|
const redPixel =
|
|
1466
|
-
|
|
1428
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
|
1467
1429
|
|
|
1468
1430
|
const doc = Document({
|
|
1469
1431
|
children: Page({
|
|
1470
|
-
children: [
|
|
1471
|
-
Image({ src: redPixel, width: 50, height: 50, caption: 'Red pixel' }),
|
|
1472
|
-
],
|
|
1432
|
+
children: [Image({ src: redPixel, width: 50, height: 50, caption: "Red pixel" })],
|
|
1473
1433
|
}),
|
|
1474
1434
|
})
|
|
1475
1435
|
|
|
1476
|
-
const result = await render(doc,
|
|
1436
|
+
const result = await render(doc, "docx")
|
|
1477
1437
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1478
1438
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1479
1439
|
}, 15000)
|
|
1480
1440
|
|
|
1481
|
-
it(
|
|
1441
|
+
it("renders external URL images as placeholders", async () => {
|
|
1482
1442
|
const doc = Document({
|
|
1483
1443
|
children: Image({
|
|
1484
|
-
src:
|
|
1485
|
-
alt:
|
|
1486
|
-
caption:
|
|
1444
|
+
src: "https://example.com/logo.png",
|
|
1445
|
+
alt: "Logo",
|
|
1446
|
+
caption: "Company",
|
|
1487
1447
|
}),
|
|
1488
1448
|
})
|
|
1489
1449
|
|
|
1490
|
-
const result = await render(doc,
|
|
1450
|
+
const result = await render(doc, "docx")
|
|
1491
1451
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1492
1452
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1493
1453
|
}, 15000)
|
|
1494
1454
|
|
|
1495
|
-
it(
|
|
1455
|
+
it("renders page with header and footer", async () => {
|
|
1496
1456
|
const doc = Document({
|
|
1497
1457
|
children: Page({
|
|
1498
|
-
header: Text({ children:
|
|
1499
|
-
footer: Text({ children:
|
|
1500
|
-
children: [
|
|
1501
|
-
Heading({ children: 'Content' }),
|
|
1502
|
-
Text({ children: 'Body text.' }),
|
|
1503
|
-
],
|
|
1458
|
+
header: Text({ children: "My Header" }),
|
|
1459
|
+
footer: Text({ children: "Page Footer" }),
|
|
1460
|
+
children: [Heading({ children: "Content" }), Text({ children: "Body text." })],
|
|
1504
1461
|
}),
|
|
1505
1462
|
})
|
|
1506
1463
|
|
|
1507
|
-
const result = await render(doc,
|
|
1464
|
+
const result = await render(doc, "docx")
|
|
1508
1465
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1509
1466
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1510
1467
|
}, 15000)
|
|
1511
1468
|
|
|
1512
|
-
it(
|
|
1469
|
+
it("renders table with bordered option and column widths", async () => {
|
|
1513
1470
|
const doc = Document({
|
|
1514
1471
|
children: Table({
|
|
1515
1472
|
columns: [
|
|
1516
|
-
{ header:
|
|
1517
|
-
{ header:
|
|
1473
|
+
{ header: "Name", width: "60%" },
|
|
1474
|
+
{ header: "Price", width: "40%", align: "right" },
|
|
1518
1475
|
],
|
|
1519
|
-
rows: [[
|
|
1476
|
+
rows: [["Widget", "$10"]],
|
|
1520
1477
|
bordered: true,
|
|
1521
1478
|
}),
|
|
1522
1479
|
})
|
|
1523
1480
|
|
|
1524
|
-
const result = await render(doc,
|
|
1481
|
+
const result = await render(doc, "docx")
|
|
1525
1482
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1526
1483
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1527
1484
|
}, 15000)
|
|
1528
1485
|
|
|
1529
|
-
it(
|
|
1486
|
+
it("renders nested lists", async () => {
|
|
1530
1487
|
const doc = Document({
|
|
1531
1488
|
children: List({
|
|
1532
1489
|
children: [
|
|
1533
1490
|
ListItem({
|
|
1534
1491
|
children: [
|
|
1535
|
-
|
|
1492
|
+
"Parent item",
|
|
1536
1493
|
List({
|
|
1537
|
-
children: [ListItem({ children:
|
|
1494
|
+
children: [ListItem({ children: "Child item" })],
|
|
1538
1495
|
}),
|
|
1539
1496
|
],
|
|
1540
1497
|
}),
|
|
@@ -1542,7 +1499,7 @@ describe('DOCX renderer', () => {
|
|
|
1542
1499
|
}),
|
|
1543
1500
|
})
|
|
1544
1501
|
|
|
1545
|
-
const result = await render(doc,
|
|
1502
|
+
const result = await render(doc, "docx")
|
|
1546
1503
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1547
1504
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1548
1505
|
}, 15000)
|
|
@@ -1550,28 +1507,28 @@ describe('DOCX renderer', () => {
|
|
|
1550
1507
|
|
|
1551
1508
|
// ─── XLSX Renderer (integration) ────────────────────────────────────────────
|
|
1552
1509
|
|
|
1553
|
-
describe(
|
|
1554
|
-
it(
|
|
1510
|
+
describe("XLSX renderer", () => {
|
|
1511
|
+
it("renders a document with tables to a valid Uint8Array", async () => {
|
|
1555
1512
|
const doc = Document({
|
|
1556
|
-
title:
|
|
1557
|
-
author:
|
|
1513
|
+
title: "XLSX Test",
|
|
1514
|
+
author: "Test Suite",
|
|
1558
1515
|
children: Page({
|
|
1559
1516
|
children: [
|
|
1560
|
-
Heading({ children:
|
|
1517
|
+
Heading({ children: "Sales Data" }),
|
|
1561
1518
|
Table({
|
|
1562
|
-
columns: [
|
|
1519
|
+
columns: ["Product", "Revenue", "Margin"],
|
|
1563
1520
|
rows: [
|
|
1564
|
-
[
|
|
1565
|
-
[
|
|
1521
|
+
["Widget", "$1,234.56", "15%"],
|
|
1522
|
+
["Gadget", "$2,500.00", "22.5%"],
|
|
1566
1523
|
],
|
|
1567
1524
|
striped: true,
|
|
1568
|
-
headerStyle: { background:
|
|
1525
|
+
headerStyle: { background: "#1a1a2e", color: "#ffffff" },
|
|
1569
1526
|
}),
|
|
1570
1527
|
],
|
|
1571
1528
|
}),
|
|
1572
1529
|
})
|
|
1573
1530
|
|
|
1574
|
-
const result = await render(doc,
|
|
1531
|
+
const result = await render(doc, "xlsx")
|
|
1575
1532
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1576
1533
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1577
1534
|
// XLSX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
|
|
@@ -1579,75 +1536,75 @@ describe('XLSX renderer', () => {
|
|
|
1579
1536
|
expect((result as Uint8Array)[1]).toBe(0x4b)
|
|
1580
1537
|
}, 15000)
|
|
1581
1538
|
|
|
1582
|
-
it(
|
|
1539
|
+
it("parses currency values as numbers", async () => {
|
|
1583
1540
|
const doc = Document({
|
|
1584
1541
|
children: Table({
|
|
1585
|
-
columns: [
|
|
1586
|
-
rows: [[
|
|
1542
|
+
columns: ["Amount"],
|
|
1543
|
+
rows: [["$1,234.56"], ["$500"], ["-$100.50"]],
|
|
1587
1544
|
}),
|
|
1588
1545
|
})
|
|
1589
1546
|
|
|
1590
|
-
const result = await render(doc,
|
|
1547
|
+
const result = await render(doc, "xlsx")
|
|
1591
1548
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1592
1549
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1593
1550
|
}, 15000)
|
|
1594
1551
|
|
|
1595
|
-
it(
|
|
1552
|
+
it("parses percentage values", async () => {
|
|
1596
1553
|
const doc = Document({
|
|
1597
1554
|
children: Table({
|
|
1598
|
-
columns: [
|
|
1599
|
-
rows: [[
|
|
1555
|
+
columns: ["Rate"],
|
|
1556
|
+
rows: [["45%"], ["12.5%"], ["-3%"]],
|
|
1600
1557
|
}),
|
|
1601
1558
|
})
|
|
1602
1559
|
|
|
1603
|
-
const result = await render(doc,
|
|
1560
|
+
const result = await render(doc, "xlsx")
|
|
1604
1561
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1605
1562
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1606
1563
|
}, 15000)
|
|
1607
1564
|
|
|
1608
|
-
it(
|
|
1565
|
+
it("renders multiple tables on the same sheet with spacing", async () => {
|
|
1609
1566
|
const doc = Document({
|
|
1610
1567
|
children: [
|
|
1611
|
-
Heading({ children:
|
|
1568
|
+
Heading({ children: "Report" }),
|
|
1612
1569
|
Table({
|
|
1613
|
-
columns: [
|
|
1570
|
+
columns: ["A", "B"],
|
|
1614
1571
|
rows: [
|
|
1615
|
-
[
|
|
1616
|
-
[
|
|
1572
|
+
["1", "2"],
|
|
1573
|
+
["3", "4"],
|
|
1617
1574
|
],
|
|
1618
|
-
caption:
|
|
1575
|
+
caption: "First Table",
|
|
1619
1576
|
}),
|
|
1620
1577
|
Table({
|
|
1621
|
-
columns: [
|
|
1622
|
-
rows: [[
|
|
1623
|
-
caption:
|
|
1578
|
+
columns: ["X", "Y"],
|
|
1579
|
+
rows: [["a", "b"]],
|
|
1580
|
+
caption: "Second Table",
|
|
1624
1581
|
}),
|
|
1625
1582
|
],
|
|
1626
1583
|
})
|
|
1627
1584
|
|
|
1628
|
-
const result = await render(doc,
|
|
1585
|
+
const result = await render(doc, "xlsx")
|
|
1629
1586
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1630
1587
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1631
1588
|
}, 15000)
|
|
1632
1589
|
|
|
1633
|
-
it(
|
|
1590
|
+
it("renders bordered tables", async () => {
|
|
1634
1591
|
const doc = Document({
|
|
1635
1592
|
children: Table({
|
|
1636
|
-
columns: [
|
|
1637
|
-
rows: [[
|
|
1593
|
+
columns: ["Name", "Value"],
|
|
1594
|
+
rows: [["Alpha", "100"]],
|
|
1638
1595
|
bordered: true,
|
|
1639
1596
|
}),
|
|
1640
1597
|
})
|
|
1641
1598
|
|
|
1642
|
-
const result = await render(doc,
|
|
1599
|
+
const result = await render(doc, "xlsx")
|
|
1643
1600
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1644
1601
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1645
1602
|
}, 15000)
|
|
1646
1603
|
|
|
1647
|
-
it(
|
|
1648
|
-
const doc = Document({ children: Text({ children:
|
|
1604
|
+
it("renders empty document with default sheet", async () => {
|
|
1605
|
+
const doc = Document({ children: Text({ children: "no tables" }) })
|
|
1649
1606
|
|
|
1650
|
-
const result = await render(doc,
|
|
1607
|
+
const result = await render(doc, "xlsx")
|
|
1651
1608
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1652
1609
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1653
1610
|
}, 15000)
|
|
@@ -1655,82 +1612,76 @@ describe('XLSX renderer', () => {
|
|
|
1655
1612
|
|
|
1656
1613
|
// ─── PDF Renderer (integration) ─────────────────────────────────────────────
|
|
1657
1614
|
|
|
1658
|
-
describe(
|
|
1659
|
-
it(
|
|
1615
|
+
describe("PDF renderer", () => {
|
|
1616
|
+
it("renders a document with heading, text, table, and data: image to a valid Uint8Array", async () => {
|
|
1660
1617
|
// 1x1 red pixel PNG as base64
|
|
1661
1618
|
const redPixel =
|
|
1662
|
-
|
|
1619
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
|
1663
1620
|
|
|
1664
1621
|
const doc = Document({
|
|
1665
|
-
title:
|
|
1666
|
-
author:
|
|
1622
|
+
title: "PDF Test",
|
|
1623
|
+
author: "Test Suite",
|
|
1667
1624
|
children: Page({
|
|
1668
|
-
size:
|
|
1625
|
+
size: "A4",
|
|
1669
1626
|
margin: 40,
|
|
1670
1627
|
children: [
|
|
1671
|
-
Heading({ children:
|
|
1672
|
-
Text({ children:
|
|
1628
|
+
Heading({ children: "PDF Integration Test" }),
|
|
1629
|
+
Text({ children: "This is a test paragraph.", bold: true }),
|
|
1673
1630
|
Table({
|
|
1674
|
-
columns: [
|
|
1631
|
+
columns: ["Name", "Value"],
|
|
1675
1632
|
rows: [
|
|
1676
|
-
[
|
|
1677
|
-
[
|
|
1633
|
+
["Alpha", "100"],
|
|
1634
|
+
["Beta", "200"],
|
|
1678
1635
|
],
|
|
1679
1636
|
striped: true,
|
|
1680
|
-
headerStyle: { background:
|
|
1637
|
+
headerStyle: { background: "#333333", color: "#ffffff" },
|
|
1681
1638
|
}),
|
|
1682
1639
|
Image({ src: redPixel, width: 50, height: 50 }),
|
|
1683
1640
|
List({
|
|
1684
1641
|
ordered: true,
|
|
1685
|
-
children: [
|
|
1686
|
-
ListItem({ children: 'First' }),
|
|
1687
|
-
ListItem({ children: 'Second' }),
|
|
1688
|
-
],
|
|
1642
|
+
children: [ListItem({ children: "First" }), ListItem({ children: "Second" })],
|
|
1689
1643
|
}),
|
|
1690
1644
|
Divider(),
|
|
1691
|
-
Quote({ children:
|
|
1645
|
+
Quote({ children: "A wise quote." }),
|
|
1692
1646
|
],
|
|
1693
1647
|
}),
|
|
1694
1648
|
})
|
|
1695
1649
|
|
|
1696
|
-
const result = await render(doc,
|
|
1650
|
+
const result = await render(doc, "pdf")
|
|
1697
1651
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1698
1652
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1699
1653
|
// PDF files start with %PDF
|
|
1700
1654
|
const header = String.fromCharCode(...(result as Uint8Array).slice(0, 5))
|
|
1701
|
-
expect(header).toBe(
|
|
1655
|
+
expect(header).toBe("%PDF-")
|
|
1702
1656
|
}, 15000)
|
|
1703
1657
|
|
|
1704
|
-
it(
|
|
1658
|
+
it("renders images with HTTP URLs as placeholder text", async () => {
|
|
1705
1659
|
const doc = Document({
|
|
1706
|
-
title:
|
|
1660
|
+
title: "HTTP Image Test",
|
|
1707
1661
|
children: Page({
|
|
1708
1662
|
children: [
|
|
1709
|
-
Heading({ children:
|
|
1710
|
-
Image({ src:
|
|
1663
|
+
Heading({ children: "Test" }),
|
|
1664
|
+
Image({ src: "https://example.com/image.png", width: 100 }),
|
|
1711
1665
|
],
|
|
1712
1666
|
}),
|
|
1713
1667
|
})
|
|
1714
1668
|
|
|
1715
|
-
const result = await render(doc,
|
|
1669
|
+
const result = await render(doc, "pdf")
|
|
1716
1670
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1717
1671
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1718
1672
|
}, 15000)
|
|
1719
1673
|
|
|
1720
|
-
it(
|
|
1674
|
+
it("renders page with header and footer", async () => {
|
|
1721
1675
|
const doc = Document({
|
|
1722
|
-
title:
|
|
1676
|
+
title: "Header/Footer Test",
|
|
1723
1677
|
children: Page({
|
|
1724
|
-
header: Text({ children:
|
|
1725
|
-
footer: Text({ children:
|
|
1726
|
-
children: [
|
|
1727
|
-
Heading({ children: 'Content' }),
|
|
1728
|
-
Text({ children: 'Body text.' }),
|
|
1729
|
-
],
|
|
1678
|
+
header: Text({ children: "Page Header", bold: true }),
|
|
1679
|
+
footer: Text({ children: "Page Footer", size: 10 }),
|
|
1680
|
+
children: [Heading({ children: "Content" }), Text({ children: "Body text." })],
|
|
1730
1681
|
}),
|
|
1731
1682
|
})
|
|
1732
1683
|
|
|
1733
|
-
const result = await render(doc,
|
|
1684
|
+
const result = await render(doc, "pdf")
|
|
1734
1685
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1735
1686
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1736
1687
|
}, 15000)
|
|
@@ -1738,34 +1689,31 @@ describe('PDF renderer', () => {
|
|
|
1738
1689
|
|
|
1739
1690
|
// ─── PPTX Renderer (integration) ────────────────────────────────────────────
|
|
1740
1691
|
|
|
1741
|
-
describe(
|
|
1742
|
-
it(
|
|
1692
|
+
describe("PPTX renderer", () => {
|
|
1693
|
+
it("renders a document with pages, headings, text, and tables to a valid Uint8Array", async () => {
|
|
1743
1694
|
const doc = Document({
|
|
1744
|
-
title:
|
|
1745
|
-
author:
|
|
1695
|
+
title: "PPTX Test",
|
|
1696
|
+
author: "Test Suite",
|
|
1746
1697
|
children: [
|
|
1747
1698
|
Page({
|
|
1748
1699
|
children: [
|
|
1749
|
-
Heading({ children:
|
|
1750
|
-
Text({ children:
|
|
1700
|
+
Heading({ children: "Slide 1 Title" }),
|
|
1701
|
+
Text({ children: "Introduction text.", bold: true }),
|
|
1751
1702
|
List({
|
|
1752
|
-
children: [
|
|
1753
|
-
ListItem({ children: 'Point A' }),
|
|
1754
|
-
ListItem({ children: 'Point B' }),
|
|
1755
|
-
],
|
|
1703
|
+
children: [ListItem({ children: "Point A" }), ListItem({ children: "Point B" })],
|
|
1756
1704
|
}),
|
|
1757
1705
|
],
|
|
1758
1706
|
}),
|
|
1759
1707
|
Page({
|
|
1760
1708
|
children: [
|
|
1761
|
-
Heading({ level: 2, children:
|
|
1709
|
+
Heading({ level: 2, children: "Slide 2 Data" }),
|
|
1762
1710
|
Table({
|
|
1763
|
-
columns: [
|
|
1711
|
+
columns: ["Metric", "Value"],
|
|
1764
1712
|
rows: [
|
|
1765
|
-
[
|
|
1766
|
-
[
|
|
1713
|
+
["Revenue", "$1M"],
|
|
1714
|
+
["Profit", "$300K"],
|
|
1767
1715
|
],
|
|
1768
|
-
headerStyle: { background:
|
|
1716
|
+
headerStyle: { background: "#1a1a2e", color: "#ffffff" },
|
|
1769
1717
|
striped: true,
|
|
1770
1718
|
}),
|
|
1771
1719
|
],
|
|
@@ -1773,7 +1721,7 @@ describe('PPTX renderer', () => {
|
|
|
1773
1721
|
],
|
|
1774
1722
|
})
|
|
1775
1723
|
|
|
1776
|
-
const result = await render(doc,
|
|
1724
|
+
const result = await render(doc, "pptx")
|
|
1777
1725
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1778
1726
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1779
1727
|
// PPTX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
|
|
@@ -1781,44 +1729,44 @@ describe('PPTX renderer', () => {
|
|
|
1781
1729
|
expect((result as Uint8Array)[1]).toBe(0x4b)
|
|
1782
1730
|
}, 15000)
|
|
1783
1731
|
|
|
1784
|
-
it(
|
|
1732
|
+
it("renders a document without explicit pages as a single slide", async () => {
|
|
1785
1733
|
const doc = Document({
|
|
1786
|
-
title:
|
|
1734
|
+
title: "Single Slide",
|
|
1787
1735
|
children: [
|
|
1788
|
-
Heading({ children:
|
|
1789
|
-
Text({ children:
|
|
1736
|
+
Heading({ children: "Auto Slide" }),
|
|
1737
|
+
Text({ children: "No explicit page wrapper." }),
|
|
1790
1738
|
],
|
|
1791
1739
|
})
|
|
1792
1740
|
|
|
1793
|
-
const result = await render(doc,
|
|
1741
|
+
const result = await render(doc, "pptx")
|
|
1794
1742
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1795
1743
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1796
1744
|
}, 15000)
|
|
1797
1745
|
|
|
1798
|
-
it(
|
|
1746
|
+
it("renders all node types without errors", async () => {
|
|
1799
1747
|
// 1x1 red pixel PNG as base64
|
|
1800
1748
|
const redPixel =
|
|
1801
|
-
|
|
1749
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
|
1802
1750
|
|
|
1803
1751
|
const doc = Document({
|
|
1804
1752
|
children: Page({
|
|
1805
1753
|
children: [
|
|
1806
|
-
Heading({ children:
|
|
1807
|
-
Text({ children:
|
|
1754
|
+
Heading({ children: "Title" }),
|
|
1755
|
+
Text({ children: "Body", bold: true, italic: true }),
|
|
1808
1756
|
Image({ src: redPixel, width: 50, height: 50 }),
|
|
1809
|
-
Code({ children:
|
|
1810
|
-
Quote({ children:
|
|
1811
|
-
Link({ href:
|
|
1812
|
-
Button({ href:
|
|
1757
|
+
Code({ children: "const x = 1" }),
|
|
1758
|
+
Quote({ children: "A quote" }),
|
|
1759
|
+
Link({ href: "https://example.com", children: "Link text" }),
|
|
1760
|
+
Button({ href: "/action", background: "#4f46e5", children: "Click" }),
|
|
1813
1761
|
Divider(),
|
|
1814
1762
|
Spacer({ height: 20 }),
|
|
1815
|
-
List({ ordered: true, children: [ListItem({ children:
|
|
1816
|
-
Section({ children: Text({ children:
|
|
1763
|
+
List({ ordered: true, children: [ListItem({ children: "one" })] }),
|
|
1764
|
+
Section({ children: Text({ children: "nested" }) }),
|
|
1817
1765
|
],
|
|
1818
1766
|
}),
|
|
1819
1767
|
})
|
|
1820
1768
|
|
|
1821
|
-
const result = await render(doc,
|
|
1769
|
+
const result = await render(doc, "pptx")
|
|
1822
1770
|
expect(result).toBeInstanceOf(Uint8Array)
|
|
1823
1771
|
expect((result as Uint8Array).length).toBeGreaterThan(0)
|
|
1824
1772
|
}, 15000)
|
|
@@ -1826,227 +1774,224 @@ describe('PPTX renderer', () => {
|
|
|
1826
1774
|
|
|
1827
1775
|
// ─── Slack Renderer ─────────────────────────────────────────────────────────
|
|
1828
1776
|
|
|
1829
|
-
describe(
|
|
1830
|
-
it(
|
|
1831
|
-
const doc = Document({ children: Heading({ children:
|
|
1832
|
-
const json = (await render(doc,
|
|
1777
|
+
describe("Slack renderer", () => {
|
|
1778
|
+
it("renders heading as header block", async () => {
|
|
1779
|
+
const doc = Document({ children: Heading({ children: "Hello" }) })
|
|
1780
|
+
const json = (await render(doc, "slack")) as string
|
|
1833
1781
|
const parsed = JSON.parse(json)
|
|
1834
1782
|
expect(parsed.blocks).toHaveLength(1)
|
|
1835
|
-
expect(parsed.blocks[0].type).toBe(
|
|
1783
|
+
expect(parsed.blocks[0].type).toBe("header")
|
|
1836
1784
|
})
|
|
1837
1785
|
|
|
1838
|
-
it(
|
|
1786
|
+
it("renders text with bold as mrkdwn", async () => {
|
|
1839
1787
|
const doc = Document({
|
|
1840
|
-
children: Text({ bold: true, children:
|
|
1788
|
+
children: Text({ bold: true, children: "Bold text" }),
|
|
1841
1789
|
})
|
|
1842
|
-
const json = (await render(doc,
|
|
1790
|
+
const json = (await render(doc, "slack")) as string
|
|
1843
1791
|
const parsed = JSON.parse(json)
|
|
1844
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1792
|
+
expect(parsed.blocks[0].text.text).toContain("*Bold text*")
|
|
1845
1793
|
})
|
|
1846
1794
|
|
|
1847
|
-
it(
|
|
1795
|
+
it("renders button as actions block", async () => {
|
|
1848
1796
|
const doc = Document({
|
|
1849
|
-
children: Button({ href:
|
|
1797
|
+
children: Button({ href: "/go", children: "Click" }),
|
|
1850
1798
|
})
|
|
1851
|
-
const json = (await render(doc,
|
|
1799
|
+
const json = (await render(doc, "slack")) as string
|
|
1852
1800
|
const parsed = JSON.parse(json)
|
|
1853
|
-
expect(parsed.blocks[0].type).toBe(
|
|
1854
|
-
expect(parsed.blocks[0].elements[0].type).toBe(
|
|
1801
|
+
expect(parsed.blocks[0].type).toBe("actions")
|
|
1802
|
+
expect(parsed.blocks[0].elements[0].type).toBe("button")
|
|
1855
1803
|
})
|
|
1856
1804
|
|
|
1857
|
-
it(
|
|
1805
|
+
it("renders table as code block", async () => {
|
|
1858
1806
|
const doc = Document({
|
|
1859
|
-
children: Table({ columns: [
|
|
1807
|
+
children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
|
|
1860
1808
|
})
|
|
1861
|
-
const json = (await render(doc,
|
|
1809
|
+
const json = (await render(doc, "slack")) as string
|
|
1862
1810
|
const parsed = JSON.parse(json)
|
|
1863
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1864
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1811
|
+
expect(parsed.blocks[0].text.text).toContain("*A*")
|
|
1812
|
+
expect(parsed.blocks[0].text.text).toContain("1 | 2")
|
|
1865
1813
|
})
|
|
1866
1814
|
|
|
1867
|
-
it(
|
|
1815
|
+
it("renders divider", async () => {
|
|
1868
1816
|
const doc = Document({ children: Divider() })
|
|
1869
|
-
const json = (await render(doc,
|
|
1817
|
+
const json = (await render(doc, "slack")) as string
|
|
1870
1818
|
const parsed = JSON.parse(json)
|
|
1871
|
-
expect(parsed.blocks[0].type).toBe(
|
|
1819
|
+
expect(parsed.blocks[0].type).toBe("divider")
|
|
1872
1820
|
})
|
|
1873
1821
|
|
|
1874
|
-
it(
|
|
1822
|
+
it("renders list as bullet points", async () => {
|
|
1875
1823
|
const doc = Document({
|
|
1876
1824
|
children: List({
|
|
1877
|
-
children: [
|
|
1878
|
-
ListItem({ children: 'one' }),
|
|
1879
|
-
ListItem({ children: 'two' }),
|
|
1880
|
-
],
|
|
1825
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
1881
1826
|
}),
|
|
1882
1827
|
})
|
|
1883
|
-
const json = (await render(doc,
|
|
1828
|
+
const json = (await render(doc, "slack")) as string
|
|
1884
1829
|
const parsed = JSON.parse(json)
|
|
1885
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1886
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1830
|
+
expect(parsed.blocks[0].text.text).toContain("• one")
|
|
1831
|
+
expect(parsed.blocks[0].text.text).toContain("• two")
|
|
1887
1832
|
})
|
|
1888
1833
|
|
|
1889
|
-
it(
|
|
1834
|
+
it("renders ordered list", async () => {
|
|
1890
1835
|
const doc = Document({
|
|
1891
1836
|
children: List({
|
|
1892
1837
|
ordered: true,
|
|
1893
|
-
children: [ListItem({ children:
|
|
1838
|
+
children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
|
|
1894
1839
|
}),
|
|
1895
1840
|
})
|
|
1896
|
-
const json = (await render(doc,
|
|
1841
|
+
const json = (await render(doc, "slack")) as string
|
|
1897
1842
|
const parsed = JSON.parse(json)
|
|
1898
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1899
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1843
|
+
expect(parsed.blocks[0].text.text).toContain("1. a")
|
|
1844
|
+
expect(parsed.blocks[0].text.text).toContain("2. b")
|
|
1900
1845
|
})
|
|
1901
1846
|
|
|
1902
|
-
it(
|
|
1847
|
+
it("renders link in mrkdwn format", async () => {
|
|
1903
1848
|
const doc = Document({
|
|
1904
|
-
children: Link({ href:
|
|
1849
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
1905
1850
|
})
|
|
1906
|
-
const json = (await render(doc,
|
|
1851
|
+
const json = (await render(doc, "slack")) as string
|
|
1907
1852
|
const parsed = JSON.parse(json)
|
|
1908
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1853
|
+
expect(parsed.blocks[0].text.text).toContain("<https://x.com|X>")
|
|
1909
1854
|
})
|
|
1910
1855
|
|
|
1911
|
-
it(
|
|
1856
|
+
it("renders code block", async () => {
|
|
1912
1857
|
const doc = Document({
|
|
1913
|
-
children: Code({ language:
|
|
1858
|
+
children: Code({ language: "js", children: "const x = 1" }),
|
|
1914
1859
|
})
|
|
1915
|
-
const json = (await render(doc,
|
|
1860
|
+
const json = (await render(doc, "slack")) as string
|
|
1916
1861
|
const parsed = JSON.parse(json)
|
|
1917
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1918
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1862
|
+
expect(parsed.blocks[0].text.text).toContain("```js")
|
|
1863
|
+
expect(parsed.blocks[0].text.text).toContain("const x = 1")
|
|
1919
1864
|
})
|
|
1920
1865
|
|
|
1921
|
-
it(
|
|
1922
|
-
const doc = Document({ children: Quote({ children:
|
|
1923
|
-
const json = (await render(doc,
|
|
1866
|
+
it("renders quote with >", async () => {
|
|
1867
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
1868
|
+
const json = (await render(doc, "slack")) as string
|
|
1924
1869
|
const parsed = JSON.parse(json)
|
|
1925
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1870
|
+
expect(parsed.blocks[0].text.text).toContain("> wise")
|
|
1926
1871
|
})
|
|
1927
1872
|
|
|
1928
|
-
it(
|
|
1873
|
+
it("renders image with URL", async () => {
|
|
1929
1874
|
const doc = Document({
|
|
1930
|
-
children: Image({ src:
|
|
1875
|
+
children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
|
|
1931
1876
|
})
|
|
1932
|
-
const json = (await render(doc,
|
|
1877
|
+
const json = (await render(doc, "slack")) as string
|
|
1933
1878
|
const parsed = JSON.parse(json)
|
|
1934
|
-
expect(parsed.blocks[0].type).toBe(
|
|
1935
|
-
expect(parsed.blocks[0].image_url).toBe(
|
|
1879
|
+
expect(parsed.blocks[0].type).toBe("image")
|
|
1880
|
+
expect(parsed.blocks[0].image_url).toBe("https://x.com/img.png")
|
|
1936
1881
|
})
|
|
1937
1882
|
|
|
1938
|
-
it(
|
|
1883
|
+
it("skips non-URL images", async () => {
|
|
1939
1884
|
const doc = Document({
|
|
1940
|
-
children: Image({ src:
|
|
1885
|
+
children: Image({ src: "data:image/png;base64,abc" }),
|
|
1941
1886
|
})
|
|
1942
|
-
const json = (await render(doc,
|
|
1887
|
+
const json = (await render(doc, "slack")) as string
|
|
1943
1888
|
const parsed = JSON.parse(json)
|
|
1944
1889
|
expect(parsed.blocks).toHaveLength(0)
|
|
1945
1890
|
})
|
|
1946
1891
|
|
|
1947
|
-
it(
|
|
1892
|
+
it("renders page-break as divider", async () => {
|
|
1948
1893
|
const doc = Document({ children: PageBreak() })
|
|
1949
|
-
const json = (await render(doc,
|
|
1894
|
+
const json = (await render(doc, "slack")) as string
|
|
1950
1895
|
const parsed = JSON.parse(json)
|
|
1951
|
-
expect(parsed.blocks[0].type).toBe(
|
|
1896
|
+
expect(parsed.blocks[0].type).toBe("divider")
|
|
1952
1897
|
})
|
|
1953
1898
|
|
|
1954
|
-
it(
|
|
1899
|
+
it("renders text with italic and strikethrough", async () => {
|
|
1955
1900
|
const doc = Document({
|
|
1956
1901
|
children: [
|
|
1957
|
-
Text({ italic: true, children:
|
|
1958
|
-
Text({ strikethrough: true, children:
|
|
1902
|
+
Text({ italic: true, children: "italic" }),
|
|
1903
|
+
Text({ strikethrough: true, children: "struck" }),
|
|
1959
1904
|
],
|
|
1960
1905
|
})
|
|
1961
|
-
const json = (await render(doc,
|
|
1906
|
+
const json = (await render(doc, "slack")) as string
|
|
1962
1907
|
const parsed = JSON.parse(json)
|
|
1963
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1964
|
-
expect(parsed.blocks[1].text.text).toContain(
|
|
1908
|
+
expect(parsed.blocks[0].text.text).toContain("_italic_")
|
|
1909
|
+
expect(parsed.blocks[1].text.text).toContain("~struck~")
|
|
1965
1910
|
})
|
|
1966
1911
|
|
|
1967
|
-
it(
|
|
1912
|
+
it("renders image with caption", async () => {
|
|
1968
1913
|
const doc = Document({
|
|
1969
|
-
children: Image({ src:
|
|
1914
|
+
children: Image({ src: "https://x.com/img.png", caption: "Nice" }),
|
|
1970
1915
|
})
|
|
1971
|
-
const json = (await render(doc,
|
|
1916
|
+
const json = (await render(doc, "slack")) as string
|
|
1972
1917
|
const parsed = JSON.parse(json)
|
|
1973
|
-
expect(parsed.blocks[0].title.text).toBe(
|
|
1918
|
+
expect(parsed.blocks[0].title.text).toBe("Nice")
|
|
1974
1919
|
})
|
|
1975
1920
|
|
|
1976
|
-
it(
|
|
1921
|
+
it("renders table with caption", async () => {
|
|
1977
1922
|
const doc = Document({
|
|
1978
|
-
children: Table({ columns: [
|
|
1923
|
+
children: Table({ columns: ["X"], rows: [["1"]], caption: "My Table" }),
|
|
1979
1924
|
})
|
|
1980
|
-
const json = (await render(doc,
|
|
1925
|
+
const json = (await render(doc, "slack")) as string
|
|
1981
1926
|
const parsed = JSON.parse(json)
|
|
1982
|
-
expect(parsed.blocks[0].text.text).toContain(
|
|
1927
|
+
expect(parsed.blocks[0].text.text).toContain("_My Table_")
|
|
1983
1928
|
})
|
|
1984
1929
|
})
|
|
1985
1930
|
|
|
1986
1931
|
// ─── PageBreak ──────────────────────────────────────────────────────────────
|
|
1987
1932
|
|
|
1988
|
-
describe(
|
|
1989
|
-
it(
|
|
1933
|
+
describe("PageBreak", () => {
|
|
1934
|
+
it("creates a page-break node", () => {
|
|
1990
1935
|
const pb = PageBreak()
|
|
1991
|
-
expect(pb.type).toBe(
|
|
1936
|
+
expect(pb.type).toBe("page-break")
|
|
1992
1937
|
expect(pb.children).toEqual([])
|
|
1993
1938
|
})
|
|
1994
1939
|
|
|
1995
|
-
it(
|
|
1940
|
+
it("renders as CSS page-break in HTML", async () => {
|
|
1996
1941
|
const doc = Document({ children: PageBreak() })
|
|
1997
|
-
const html = (await render(doc,
|
|
1998
|
-
expect(html).toContain(
|
|
1942
|
+
const html = (await render(doc, "html")) as string
|
|
1943
|
+
expect(html).toContain("page-break-after:always")
|
|
1999
1944
|
})
|
|
2000
1945
|
|
|
2001
|
-
it(
|
|
1946
|
+
it("renders as separator in email", async () => {
|
|
2002
1947
|
const doc = Document({ children: PageBreak() })
|
|
2003
|
-
const html = (await render(doc,
|
|
2004
|
-
expect(html).toContain(
|
|
1948
|
+
const html = (await render(doc, "email")) as string
|
|
1949
|
+
expect(html).toContain("border-top:2px solid")
|
|
2005
1950
|
})
|
|
2006
1951
|
|
|
2007
|
-
it(
|
|
1952
|
+
it("renders as --- in markdown", async () => {
|
|
2008
1953
|
const doc = Document({ children: PageBreak() })
|
|
2009
|
-
const md = (await render(doc,
|
|
2010
|
-
expect(md).toContain(
|
|
1954
|
+
const md = (await render(doc, "md")) as string
|
|
1955
|
+
expect(md).toContain("---")
|
|
2011
1956
|
})
|
|
2012
1957
|
|
|
2013
|
-
it(
|
|
1958
|
+
it("renders as separator in text", async () => {
|
|
2014
1959
|
const doc = Document({ children: PageBreak() })
|
|
2015
|
-
const text = (await render(doc,
|
|
2016
|
-
expect(text).toContain(
|
|
1960
|
+
const text = (await render(doc, "text")) as string
|
|
1961
|
+
expect(text).toContain("═")
|
|
2017
1962
|
})
|
|
2018
1963
|
|
|
2019
|
-
it(
|
|
2020
|
-
const doc = createDocument().heading(
|
|
1964
|
+
it("builder pageBreak inserts page-break node", async () => {
|
|
1965
|
+
const doc = createDocument().heading("Page 1").pageBreak().heading("Page 2")
|
|
2021
1966
|
const html = await doc.toHtml()
|
|
2022
|
-
expect(html).toContain(
|
|
2023
|
-
expect(html).toContain(
|
|
2024
|
-
expect(html).toContain(
|
|
1967
|
+
expect(html).toContain("page-break-after:always")
|
|
1968
|
+
expect(html).toContain("Page 1")
|
|
1969
|
+
expect(html).toContain("Page 2")
|
|
2025
1970
|
})
|
|
2026
1971
|
})
|
|
2027
1972
|
|
|
2028
1973
|
// ─── RTL Support ────────────────────────────────────────────────────────────
|
|
2029
1974
|
|
|
2030
|
-
describe(
|
|
2031
|
-
it(
|
|
2032
|
-
const doc = Document({ children: Text({ children:
|
|
2033
|
-
const html = (await render(doc,
|
|
1975
|
+
describe("RTL support", () => {
|
|
1976
|
+
it("adds dir=rtl to HTML body", async () => {
|
|
1977
|
+
const doc = Document({ children: Text({ children: "مرحبا" }) })
|
|
1978
|
+
const html = (await render(doc, "html", { direction: "rtl" })) as string
|
|
2034
1979
|
expect(html).toContain('dir="rtl"')
|
|
2035
|
-
expect(html).toContain(
|
|
1980
|
+
expect(html).toContain("direction:rtl")
|
|
2036
1981
|
})
|
|
2037
1982
|
|
|
2038
|
-
it(
|
|
2039
|
-
const doc = Document({ children: Text({ children:
|
|
2040
|
-
const html = (await render(doc,
|
|
1983
|
+
it("does not add dir for ltr (default)", async () => {
|
|
1984
|
+
const doc = Document({ children: Text({ children: "Hello" }) })
|
|
1985
|
+
const html = (await render(doc, "html")) as string
|
|
2041
1986
|
expect(html).not.toContain('dir="rtl"')
|
|
2042
1987
|
})
|
|
2043
1988
|
})
|
|
2044
1989
|
|
|
2045
1990
|
// ─── keepTogether ───────────────────────────────────────────────────────────
|
|
2046
1991
|
|
|
2047
|
-
describe(
|
|
2048
|
-
it(
|
|
2049
|
-
const t = Table({ columns: [
|
|
1992
|
+
describe("keepTogether", () => {
|
|
1993
|
+
it("table accepts keepTogether prop", () => {
|
|
1994
|
+
const t = Table({ columns: ["A"], rows: [["1"]], keepTogether: true })
|
|
2050
1995
|
expect(t.props.keepTogether).toBe(true)
|
|
2051
1996
|
})
|
|
2052
1997
|
})
|
|
@@ -2055,372 +2000,358 @@ describe('keepTogether', () => {
|
|
|
2055
2000
|
|
|
2056
2001
|
// ─── SVG Renderer ───────────────────────────────────────────────────────────
|
|
2057
2002
|
|
|
2058
|
-
describe(
|
|
2059
|
-
it(
|
|
2060
|
-
const doc = Document({ children: Heading({ children:
|
|
2061
|
-
const svg = (await render(doc,
|
|
2003
|
+
describe("SVG renderer", () => {
|
|
2004
|
+
it("renders a valid SVG document", async () => {
|
|
2005
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2006
|
+
const svg = (await render(doc, "svg")) as string
|
|
2062
2007
|
expect(svg).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
|
|
2063
|
-
expect(svg).toContain(
|
|
2064
|
-
expect(svg).toContain(
|
|
2008
|
+
expect(svg).toContain("</svg>")
|
|
2009
|
+
expect(svg).toContain("Title")
|
|
2065
2010
|
})
|
|
2066
2011
|
|
|
2067
|
-
it(
|
|
2068
|
-
const doc = Document({ children: Heading({ level: 2, children:
|
|
2069
|
-
const svg = (await render(doc,
|
|
2012
|
+
it("renders heading with correct font size", async () => {
|
|
2013
|
+
const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
|
|
2014
|
+
const svg = (await render(doc, "svg")) as string
|
|
2070
2015
|
expect(svg).toContain('font-size="24"')
|
|
2071
2016
|
expect(svg).toContain('font-weight="bold"')
|
|
2072
2017
|
})
|
|
2073
2018
|
|
|
2074
|
-
it(
|
|
2019
|
+
it("renders text with bold and italic", async () => {
|
|
2075
2020
|
const doc = Document({
|
|
2076
2021
|
children: [
|
|
2077
|
-
Text({ bold: true, children:
|
|
2078
|
-
Text({ italic: true, children:
|
|
2022
|
+
Text({ bold: true, children: "Bold" }),
|
|
2023
|
+
Text({ italic: true, children: "Italic" }),
|
|
2079
2024
|
],
|
|
2080
2025
|
})
|
|
2081
|
-
const svg = (await render(doc,
|
|
2026
|
+
const svg = (await render(doc, "svg")) as string
|
|
2082
2027
|
expect(svg).toContain('font-weight="bold"')
|
|
2083
2028
|
expect(svg).toContain('font-style="italic"')
|
|
2084
2029
|
})
|
|
2085
2030
|
|
|
2086
|
-
it(
|
|
2031
|
+
it("renders table with header and rows", async () => {
|
|
2087
2032
|
const doc = Document({
|
|
2088
2033
|
children: Table({
|
|
2089
|
-
columns: [
|
|
2090
|
-
rows: [[
|
|
2091
|
-
headerStyle: { background:
|
|
2034
|
+
columns: ["Name", "Price"],
|
|
2035
|
+
rows: [["Widget", "$10"]],
|
|
2036
|
+
headerStyle: { background: "#000", color: "#fff" },
|
|
2092
2037
|
striped: true,
|
|
2093
2038
|
}),
|
|
2094
2039
|
})
|
|
2095
|
-
const svg = (await render(doc,
|
|
2096
|
-
expect(svg).toContain(
|
|
2097
|
-
expect(svg).toContain(
|
|
2040
|
+
const svg = (await render(doc, "svg")) as string
|
|
2041
|
+
expect(svg).toContain("Name")
|
|
2042
|
+
expect(svg).toContain("Widget")
|
|
2098
2043
|
expect(svg).toContain('fill="#000"')
|
|
2099
2044
|
})
|
|
2100
2045
|
|
|
2101
|
-
it(
|
|
2046
|
+
it("renders image from data URL", async () => {
|
|
2102
2047
|
const doc = Document({
|
|
2103
2048
|
children: Image({
|
|
2104
|
-
src:
|
|
2049
|
+
src: "data:image/png;base64,abc",
|
|
2105
2050
|
width: 200,
|
|
2106
2051
|
height: 100,
|
|
2107
|
-
caption:
|
|
2052
|
+
caption: "Photo",
|
|
2108
2053
|
}),
|
|
2109
2054
|
})
|
|
2110
|
-
const svg = (await render(doc,
|
|
2111
|
-
expect(svg).toContain(
|
|
2112
|
-
expect(svg).toContain(
|
|
2113
|
-
expect(svg).toContain(
|
|
2055
|
+
const svg = (await render(doc, "svg")) as string
|
|
2056
|
+
expect(svg).toContain("<image")
|
|
2057
|
+
expect(svg).toContain("data:image/png;base64,abc")
|
|
2058
|
+
expect(svg).toContain("Photo")
|
|
2114
2059
|
})
|
|
2115
2060
|
|
|
2116
|
-
it(
|
|
2061
|
+
it("renders image placeholder for local paths", async () => {
|
|
2117
2062
|
const doc = Document({
|
|
2118
|
-
children: Image({ src:
|
|
2063
|
+
children: Image({ src: "/local.png", alt: "Local" }),
|
|
2119
2064
|
})
|
|
2120
|
-
const svg = (await render(doc,
|
|
2121
|
-
expect(svg).toContain(
|
|
2065
|
+
const svg = (await render(doc, "svg")) as string
|
|
2066
|
+
expect(svg).toContain("Local")
|
|
2122
2067
|
expect(svg).toContain('fill="#f0f0f0"')
|
|
2123
2068
|
})
|
|
2124
2069
|
|
|
2125
|
-
it(
|
|
2070
|
+
it("renders list", async () => {
|
|
2126
2071
|
const doc = Document({
|
|
2127
2072
|
children: List({
|
|
2128
2073
|
ordered: true,
|
|
2129
|
-
children: [
|
|
2130
|
-
ListItem({ children: 'one' }),
|
|
2131
|
-
ListItem({ children: 'two' }),
|
|
2132
|
-
],
|
|
2074
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
2133
2075
|
}),
|
|
2134
2076
|
})
|
|
2135
|
-
const svg = (await render(doc,
|
|
2136
|
-
expect(svg).toContain(
|
|
2137
|
-
expect(svg).toContain(
|
|
2077
|
+
const svg = (await render(doc, "svg")) as string
|
|
2078
|
+
expect(svg).toContain("1. one")
|
|
2079
|
+
expect(svg).toContain("2. two")
|
|
2138
2080
|
})
|
|
2139
2081
|
|
|
2140
|
-
it(
|
|
2082
|
+
it("renders unordered list", async () => {
|
|
2141
2083
|
const doc = Document({
|
|
2142
2084
|
children: List({
|
|
2143
|
-
children: [ListItem({ children:
|
|
2085
|
+
children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
|
|
2144
2086
|
}),
|
|
2145
2087
|
})
|
|
2146
|
-
const svg = (await render(doc,
|
|
2147
|
-
expect(svg).toContain(
|
|
2148
|
-
expect(svg).toContain(
|
|
2088
|
+
const svg = (await render(doc, "svg")) as string
|
|
2089
|
+
expect(svg).toContain("• a")
|
|
2090
|
+
expect(svg).toContain("• b")
|
|
2149
2091
|
})
|
|
2150
2092
|
|
|
2151
|
-
it(
|
|
2152
|
-
const doc = Document({ children: Code({ children:
|
|
2153
|
-
const svg = (await render(doc,
|
|
2154
|
-
expect(svg).toContain(
|
|
2093
|
+
it("renders code block", async () => {
|
|
2094
|
+
const doc = Document({ children: Code({ children: "const x = 1" }) })
|
|
2095
|
+
const svg = (await render(doc, "svg")) as string
|
|
2096
|
+
expect(svg).toContain("const x = 1")
|
|
2155
2097
|
expect(svg).toContain('font-family="monospace"')
|
|
2156
2098
|
expect(svg).toContain('fill="#f5f5f5"') // background
|
|
2157
2099
|
})
|
|
2158
2100
|
|
|
2159
|
-
it(
|
|
2160
|
-
const doc = Document({ children: Divider({ color:
|
|
2161
|
-
const svg = (await render(doc,
|
|
2101
|
+
it("renders divider", async () => {
|
|
2102
|
+
const doc = Document({ children: Divider({ color: "#ccc", thickness: 2 }) })
|
|
2103
|
+
const svg = (await render(doc, "svg")) as string
|
|
2162
2104
|
expect(svg).toContain('stroke="#ccc"')
|
|
2163
2105
|
expect(svg).toContain('stroke-width="2"')
|
|
2164
2106
|
})
|
|
2165
2107
|
|
|
2166
|
-
it(
|
|
2108
|
+
it("renders button", async () => {
|
|
2167
2109
|
const doc = Document({
|
|
2168
2110
|
children: Button({
|
|
2169
|
-
href:
|
|
2170
|
-
background:
|
|
2171
|
-
children:
|
|
2111
|
+
href: "/pay",
|
|
2112
|
+
background: "#4f46e5",
|
|
2113
|
+
children: "Pay",
|
|
2172
2114
|
}),
|
|
2173
2115
|
})
|
|
2174
|
-
const svg = (await render(doc,
|
|
2116
|
+
const svg = (await render(doc, "svg")) as string
|
|
2175
2117
|
expect(svg).toContain('fill="#4f46e5"')
|
|
2176
|
-
expect(svg).toContain(
|
|
2118
|
+
expect(svg).toContain("Pay")
|
|
2177
2119
|
})
|
|
2178
2120
|
|
|
2179
|
-
it(
|
|
2180
|
-
const doc = Document({ children: Quote({ children:
|
|
2181
|
-
const svg = (await render(doc,
|
|
2182
|
-
expect(svg).toContain(
|
|
2121
|
+
it("renders quote", async () => {
|
|
2122
|
+
const doc = Document({ children: Quote({ children: "wise words" }) })
|
|
2123
|
+
const svg = (await render(doc, "svg")) as string
|
|
2124
|
+
expect(svg).toContain("wise words")
|
|
2183
2125
|
expect(svg).toContain('font-style="italic"')
|
|
2184
2126
|
})
|
|
2185
2127
|
|
|
2186
|
-
it(
|
|
2128
|
+
it("renders link", async () => {
|
|
2187
2129
|
const doc = Document({
|
|
2188
|
-
children: Link({ href:
|
|
2130
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2189
2131
|
})
|
|
2190
|
-
const svg = (await render(doc,
|
|
2132
|
+
const svg = (await render(doc, "svg")) as string
|
|
2191
2133
|
expect(svg).toContain('<a href="https://x.com">')
|
|
2192
|
-
expect(svg).toContain(
|
|
2134
|
+
expect(svg).toContain("X")
|
|
2193
2135
|
})
|
|
2194
2136
|
|
|
2195
|
-
it(
|
|
2137
|
+
it("renders spacer", async () => {
|
|
2196
2138
|
const doc = Document({
|
|
2197
|
-
children: [
|
|
2198
|
-
Text({ children: 'A' }),
|
|
2199
|
-
Spacer({ height: 50 }),
|
|
2200
|
-
Text({ children: 'B' }),
|
|
2201
|
-
],
|
|
2139
|
+
children: [Text({ children: "A" }), Spacer({ height: 50 }), Text({ children: "B" })],
|
|
2202
2140
|
})
|
|
2203
|
-
const svg = (await render(doc,
|
|
2204
|
-
expect(svg).toContain(
|
|
2205
|
-
expect(svg).toContain(
|
|
2141
|
+
const svg = (await render(doc, "svg")) as string
|
|
2142
|
+
expect(svg).toContain("A")
|
|
2143
|
+
expect(svg).toContain("B")
|
|
2206
2144
|
})
|
|
2207
2145
|
|
|
2208
|
-
it(
|
|
2146
|
+
it("renders page-break as dashed line", async () => {
|
|
2209
2147
|
const doc = Document({ children: PageBreak() })
|
|
2210
|
-
const svg = (await render(doc,
|
|
2211
|
-
expect(svg).toContain(
|
|
2148
|
+
const svg = (await render(doc, "svg")) as string
|
|
2149
|
+
expect(svg).toContain("stroke-dasharray")
|
|
2212
2150
|
})
|
|
2213
2151
|
|
|
2214
|
-
it(
|
|
2215
|
-
const doc = Document({ children: Text({ children:
|
|
2216
|
-
const svg = (await render(doc,
|
|
2152
|
+
it("supports RTL direction", async () => {
|
|
2153
|
+
const doc = Document({ children: Text({ children: "مرحبا" }) })
|
|
2154
|
+
const svg = (await render(doc, "svg", { direction: "rtl" })) as string
|
|
2217
2155
|
expect(svg).toContain('direction="rtl"')
|
|
2218
2156
|
})
|
|
2219
2157
|
|
|
2220
|
-
it(
|
|
2158
|
+
it("auto-calculates height from content", async () => {
|
|
2221
2159
|
const doc = Document({
|
|
2222
|
-
children: [
|
|
2223
|
-
Heading({ children: 'A' }),
|
|
2224
|
-
Text({ children: 'B' }),
|
|
2225
|
-
Text({ children: 'C' }),
|
|
2226
|
-
],
|
|
2160
|
+
children: [Heading({ children: "A" }), Text({ children: "B" }), Text({ children: "C" })],
|
|
2227
2161
|
})
|
|
2228
|
-
const svg = (await render(doc,
|
|
2162
|
+
const svg = (await render(doc, "svg")) as string
|
|
2229
2163
|
const match = svg.match(/height="(\d+)"/)
|
|
2230
2164
|
expect(match).toBeTruthy()
|
|
2231
2165
|
expect(Number(match![1])).toBeGreaterThan(80)
|
|
2232
2166
|
})
|
|
2233
2167
|
|
|
2234
|
-
it(
|
|
2168
|
+
it("renders image from HTTP URL", async () => {
|
|
2235
2169
|
const doc = Document({
|
|
2236
|
-
children: Image({ src:
|
|
2170
|
+
children: Image({ src: "https://x.com/img.png", width: 300 }),
|
|
2237
2171
|
})
|
|
2238
|
-
const svg = (await render(doc,
|
|
2239
|
-
expect(svg).toContain(
|
|
2240
|
-
expect(svg).toContain(
|
|
2172
|
+
const svg = (await render(doc, "svg")) as string
|
|
2173
|
+
expect(svg).toContain("<image")
|
|
2174
|
+
expect(svg).toContain("https://x.com/img.png")
|
|
2241
2175
|
})
|
|
2242
2176
|
|
|
2243
|
-
it(
|
|
2244
|
-
const svg = await createDocument().heading(
|
|
2245
|
-
expect(svg).toContain(
|
|
2246
|
-
expect(svg).toContain(
|
|
2247
|
-
expect(svg).toContain(
|
|
2177
|
+
it("builder toSvg works", async () => {
|
|
2178
|
+
const svg = await createDocument().heading("Hi").text("World").toSvg()
|
|
2179
|
+
expect(svg).toContain("<svg")
|
|
2180
|
+
expect(svg).toContain("Hi")
|
|
2181
|
+
expect(svg).toContain("World")
|
|
2248
2182
|
})
|
|
2249
2183
|
})
|
|
2250
2184
|
|
|
2251
2185
|
// ─── Teams Renderer ─────────────────────────────────────────────────────────
|
|
2252
2186
|
|
|
2253
|
-
describe(
|
|
2254
|
-
it(
|
|
2255
|
-
const doc = Document({ children: Heading({ children:
|
|
2256
|
-
const json = (await render(doc,
|
|
2187
|
+
describe("Teams renderer", () => {
|
|
2188
|
+
it("renders heading as TextBlock", async () => {
|
|
2189
|
+
const doc = Document({ children: Heading({ children: "Hello" }) })
|
|
2190
|
+
const json = (await render(doc, "teams")) as string
|
|
2257
2191
|
const card = JSON.parse(json)
|
|
2258
|
-
expect(card.type).toBe(
|
|
2259
|
-
expect(card.body[0].type).toBe(
|
|
2260
|
-
expect(card.body[0].text).toBe(
|
|
2261
|
-
expect(card.body[0].weight).toBe(
|
|
2192
|
+
expect(card.type).toBe("AdaptiveCard")
|
|
2193
|
+
expect(card.body[0].type).toBe("TextBlock")
|
|
2194
|
+
expect(card.body[0].text).toBe("Hello")
|
|
2195
|
+
expect(card.body[0].weight).toBe("bolder")
|
|
2262
2196
|
})
|
|
2263
2197
|
|
|
2264
|
-
it(
|
|
2265
|
-
const doc = Document({ children: Text({ bold: true, children:
|
|
2266
|
-
const json = (await render(doc,
|
|
2198
|
+
it("renders bold text", async () => {
|
|
2199
|
+
const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
|
|
2200
|
+
const json = (await render(doc, "teams")) as string
|
|
2267
2201
|
const card = JSON.parse(json)
|
|
2268
|
-
expect(card.body[0].text).toContain(
|
|
2202
|
+
expect(card.body[0].text).toContain("**Bold**")
|
|
2269
2203
|
})
|
|
2270
2204
|
|
|
2271
|
-
it(
|
|
2205
|
+
it("renders button as Action.OpenUrl", async () => {
|
|
2272
2206
|
const doc = Document({
|
|
2273
|
-
children: Button({ href:
|
|
2207
|
+
children: Button({ href: "/go", children: "Click" }),
|
|
2274
2208
|
})
|
|
2275
|
-
const json = (await render(doc,
|
|
2209
|
+
const json = (await render(doc, "teams")) as string
|
|
2276
2210
|
const card = JSON.parse(json)
|
|
2277
|
-
expect(card.body[0].type).toBe(
|
|
2278
|
-
expect(card.body[0].actions[0].type).toBe(
|
|
2211
|
+
expect(card.body[0].type).toBe("ActionSet")
|
|
2212
|
+
expect(card.body[0].actions[0].type).toBe("Action.OpenUrl")
|
|
2279
2213
|
})
|
|
2280
2214
|
|
|
2281
|
-
it(
|
|
2215
|
+
it("renders table as ColumnSet", async () => {
|
|
2282
2216
|
const doc = Document({
|
|
2283
|
-
children: Table({ columns: [
|
|
2217
|
+
children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
|
|
2284
2218
|
})
|
|
2285
|
-
const json = (await render(doc,
|
|
2219
|
+
const json = (await render(doc, "teams")) as string
|
|
2286
2220
|
const card = JSON.parse(json)
|
|
2287
|
-
expect(card.body[0].type).toBe(
|
|
2221
|
+
expect(card.body[0].type).toBe("ColumnSet")
|
|
2288
2222
|
})
|
|
2289
2223
|
|
|
2290
|
-
it(
|
|
2224
|
+
it("renders list", async () => {
|
|
2291
2225
|
const doc = Document({
|
|
2292
2226
|
children: List({
|
|
2293
|
-
children: [
|
|
2294
|
-
ListItem({ children: 'one' }),
|
|
2295
|
-
ListItem({ children: 'two' }),
|
|
2296
|
-
],
|
|
2227
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
2297
2228
|
}),
|
|
2298
2229
|
})
|
|
2299
|
-
const json = (await render(doc,
|
|
2230
|
+
const json = (await render(doc, "teams")) as string
|
|
2300
2231
|
const card = JSON.parse(json)
|
|
2301
|
-
expect(card.body[0].text).toContain(
|
|
2232
|
+
expect(card.body[0].text).toContain("• one")
|
|
2302
2233
|
})
|
|
2303
2234
|
|
|
2304
|
-
it(
|
|
2305
|
-
const doc = Document({ children: Code({ children:
|
|
2306
|
-
const json = (await render(doc,
|
|
2235
|
+
it("renders code as monospace", async () => {
|
|
2236
|
+
const doc = Document({ children: Code({ children: "x = 1" }) })
|
|
2237
|
+
const json = (await render(doc, "teams")) as string
|
|
2307
2238
|
const card = JSON.parse(json)
|
|
2308
|
-
expect(card.body[0].fontType).toBe(
|
|
2239
|
+
expect(card.body[0].fontType).toBe("monospace")
|
|
2309
2240
|
})
|
|
2310
2241
|
|
|
2311
|
-
it(
|
|
2242
|
+
it("renders divider as separator", async () => {
|
|
2312
2243
|
const doc = Document({ children: Divider() })
|
|
2313
|
-
const json = (await render(doc,
|
|
2244
|
+
const json = (await render(doc, "teams")) as string
|
|
2314
2245
|
const card = JSON.parse(json)
|
|
2315
2246
|
expect(card.body[0].separator).toBe(true)
|
|
2316
2247
|
})
|
|
2317
2248
|
|
|
2318
|
-
it(
|
|
2249
|
+
it("renders image with URL", async () => {
|
|
2319
2250
|
const doc = Document({
|
|
2320
|
-
children: Image({ src:
|
|
2251
|
+
children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
|
|
2321
2252
|
})
|
|
2322
|
-
const json = (await render(doc,
|
|
2253
|
+
const json = (await render(doc, "teams")) as string
|
|
2323
2254
|
const card = JSON.parse(json)
|
|
2324
|
-
expect(card.body[0].type).toBe(
|
|
2255
|
+
expect(card.body[0].type).toBe("Image")
|
|
2325
2256
|
})
|
|
2326
2257
|
|
|
2327
|
-
it(
|
|
2328
|
-
const doc = Document({ children: Quote({ children:
|
|
2329
|
-
const json = (await render(doc,
|
|
2258
|
+
it("renders quote as Container", async () => {
|
|
2259
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2260
|
+
const json = (await render(doc, "teams")) as string
|
|
2330
2261
|
const card = JSON.parse(json)
|
|
2331
|
-
expect(card.body[0].type).toBe(
|
|
2332
|
-
expect(card.body[0].style).toBe(
|
|
2262
|
+
expect(card.body[0].type).toBe("Container")
|
|
2263
|
+
expect(card.body[0].style).toBe("emphasis")
|
|
2333
2264
|
})
|
|
2334
2265
|
|
|
2335
|
-
it(
|
|
2266
|
+
it("renders link as markdown", async () => {
|
|
2336
2267
|
const doc = Document({
|
|
2337
|
-
children: Link({ href:
|
|
2268
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2338
2269
|
})
|
|
2339
|
-
const json = (await render(doc,
|
|
2270
|
+
const json = (await render(doc, "teams")) as string
|
|
2340
2271
|
const card = JSON.parse(json)
|
|
2341
|
-
expect(card.body[0].text).toContain(
|
|
2272
|
+
expect(card.body[0].text).toContain("[X](https://x.com)")
|
|
2342
2273
|
})
|
|
2343
2274
|
|
|
2344
|
-
it(
|
|
2345
|
-
const json = await createDocument().heading(
|
|
2275
|
+
it("builder toTeams works", async () => {
|
|
2276
|
+
const json = await createDocument().heading("Hi").toTeams()
|
|
2346
2277
|
const card = JSON.parse(json)
|
|
2347
|
-
expect(card.type).toBe(
|
|
2278
|
+
expect(card.type).toBe("AdaptiveCard")
|
|
2348
2279
|
})
|
|
2349
2280
|
})
|
|
2350
2281
|
|
|
2351
2282
|
// ─── Discord Renderer ───────────────────────────────────────────────────────
|
|
2352
2283
|
|
|
2353
|
-
describe(
|
|
2354
|
-
it(
|
|
2355
|
-
const doc = Document({ children: Heading({ children:
|
|
2356
|
-
const json = (await render(doc,
|
|
2284
|
+
describe("Discord renderer", () => {
|
|
2285
|
+
it("renders heading as embed title", async () => {
|
|
2286
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2287
|
+
const json = (await render(doc, "discord")) as string
|
|
2357
2288
|
const payload = JSON.parse(json)
|
|
2358
|
-
expect(payload.embeds[0].title).toBe(
|
|
2289
|
+
expect(payload.embeds[0].title).toBe("Title")
|
|
2359
2290
|
})
|
|
2360
2291
|
|
|
2361
|
-
it(
|
|
2292
|
+
it("renders text in description", async () => {
|
|
2362
2293
|
const doc = Document({
|
|
2363
|
-
children: [Heading({ children:
|
|
2294
|
+
children: [Heading({ children: "T" }), Text({ children: "Body" })],
|
|
2364
2295
|
})
|
|
2365
|
-
const json = (await render(doc,
|
|
2296
|
+
const json = (await render(doc, "discord")) as string
|
|
2366
2297
|
const payload = JSON.parse(json)
|
|
2367
|
-
expect(payload.embeds[0].description).toContain(
|
|
2298
|
+
expect(payload.embeds[0].description).toContain("Body")
|
|
2368
2299
|
})
|
|
2369
2300
|
|
|
2370
|
-
it(
|
|
2301
|
+
it("renders small table as fields", async () => {
|
|
2371
2302
|
const doc = Document({
|
|
2372
2303
|
children: Table({
|
|
2373
|
-
columns: [
|
|
2304
|
+
columns: ["A", "B"],
|
|
2374
2305
|
rows: [
|
|
2375
|
-
[
|
|
2376
|
-
[
|
|
2306
|
+
["1", "2"],
|
|
2307
|
+
["3", "4"],
|
|
2377
2308
|
],
|
|
2378
2309
|
}),
|
|
2379
2310
|
})
|
|
2380
|
-
const json = (await render(doc,
|
|
2311
|
+
const json = (await render(doc, "discord")) as string
|
|
2381
2312
|
const payload = JSON.parse(json)
|
|
2382
2313
|
expect(payload.embeds[0].fields).toHaveLength(2)
|
|
2383
|
-
expect(payload.embeds[0].fields[0].name).toBe(
|
|
2314
|
+
expect(payload.embeds[0].fields[0].name).toBe("A")
|
|
2384
2315
|
expect(payload.embeds[0].fields[0].inline).toBe(true)
|
|
2385
2316
|
})
|
|
2386
2317
|
|
|
2387
|
-
it(
|
|
2388
|
-
const doc = Document({ children: Image({ src:
|
|
2389
|
-
const json = (await render(doc,
|
|
2318
|
+
it("renders image as embed image", async () => {
|
|
2319
|
+
const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
|
|
2320
|
+
const json = (await render(doc, "discord")) as string
|
|
2390
2321
|
const payload = JSON.parse(json)
|
|
2391
|
-
expect(payload.embeds[0].image.url).toBe(
|
|
2322
|
+
expect(payload.embeds[0].image.url).toBe("https://x.com/img.png")
|
|
2392
2323
|
})
|
|
2393
2324
|
|
|
2394
|
-
it(
|
|
2395
|
-
const doc = Document({ children: Quote({ children:
|
|
2396
|
-
const json = (await render(doc,
|
|
2325
|
+
it("renders quote with >", async () => {
|
|
2326
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2327
|
+
const json = (await render(doc, "discord")) as string
|
|
2397
2328
|
const payload = JSON.parse(json)
|
|
2398
|
-
expect(payload.embeds[0].description).toContain(
|
|
2329
|
+
expect(payload.embeds[0].description).toContain("> wise")
|
|
2399
2330
|
})
|
|
2400
2331
|
|
|
2401
|
-
it(
|
|
2332
|
+
it("renders code block", async () => {
|
|
2402
2333
|
const doc = Document({
|
|
2403
|
-
children: Code({ language:
|
|
2334
|
+
children: Code({ language: "js", children: "x()" }),
|
|
2404
2335
|
})
|
|
2405
|
-
const json = (await render(doc,
|
|
2336
|
+
const json = (await render(doc, "discord")) as string
|
|
2406
2337
|
const payload = JSON.parse(json)
|
|
2407
|
-
expect(payload.embeds[0].description).toContain(
|
|
2338
|
+
expect(payload.embeds[0].description).toContain("```js")
|
|
2408
2339
|
})
|
|
2409
2340
|
|
|
2410
|
-
it(
|
|
2341
|
+
it("renders list", async () => {
|
|
2411
2342
|
const doc = Document({
|
|
2412
2343
|
children: List({
|
|
2413
2344
|
ordered: true,
|
|
2414
|
-
children: [ListItem({ children:
|
|
2345
|
+
children: [ListItem({ children: "a" })],
|
|
2415
2346
|
}),
|
|
2416
2347
|
})
|
|
2417
|
-
const json = (await render(doc,
|
|
2348
|
+
const json = (await render(doc, "discord")) as string
|
|
2418
2349
|
const payload = JSON.parse(json)
|
|
2419
|
-
expect(payload.embeds[0].description).toContain(
|
|
2350
|
+
expect(payload.embeds[0].description).toContain("1. a")
|
|
2420
2351
|
})
|
|
2421
2352
|
|
|
2422
|
-
it(
|
|
2423
|
-
const json = await createDocument().heading(
|
|
2353
|
+
it("builder toDiscord works", async () => {
|
|
2354
|
+
const json = await createDocument().heading("Hi").text("World").toDiscord()
|
|
2424
2355
|
const payload = JSON.parse(json)
|
|
2425
2356
|
expect(payload.embeds).toHaveLength(1)
|
|
2426
2357
|
})
|
|
@@ -2428,221 +2359,214 @@ describe('Discord renderer', () => {
|
|
|
2428
2359
|
|
|
2429
2360
|
// ─── Telegram Renderer ──────────────────────────────────────────────────────
|
|
2430
2361
|
|
|
2431
|
-
describe(
|
|
2432
|
-
it(
|
|
2433
|
-
const doc = Document({ children: Heading({ children:
|
|
2434
|
-
const html = (await render(doc,
|
|
2435
|
-
expect(html).toContain(
|
|
2362
|
+
describe("Telegram renderer", () => {
|
|
2363
|
+
it("renders heading as bold", async () => {
|
|
2364
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2365
|
+
const html = (await render(doc, "telegram")) as string
|
|
2366
|
+
expect(html).toContain("<b>Title</b>")
|
|
2436
2367
|
})
|
|
2437
2368
|
|
|
2438
|
-
it(
|
|
2369
|
+
it("renders text with formatting", async () => {
|
|
2439
2370
|
const doc = Document({
|
|
2440
2371
|
children: [
|
|
2441
|
-
Text({ bold: true, children:
|
|
2442
|
-
Text({ italic: true, children:
|
|
2443
|
-
Text({ underline: true, children:
|
|
2444
|
-
Text({ strikethrough: true, children:
|
|
2372
|
+
Text({ bold: true, children: "Bold" }),
|
|
2373
|
+
Text({ italic: true, children: "Italic" }),
|
|
2374
|
+
Text({ underline: true, children: "Under" }),
|
|
2375
|
+
Text({ strikethrough: true, children: "Struck" }),
|
|
2445
2376
|
],
|
|
2446
2377
|
})
|
|
2447
|
-
const html = (await render(doc,
|
|
2448
|
-
expect(html).toContain(
|
|
2449
|
-
expect(html).toContain(
|
|
2450
|
-
expect(html).toContain(
|
|
2451
|
-
expect(html).toContain(
|
|
2378
|
+
const html = (await render(doc, "telegram")) as string
|
|
2379
|
+
expect(html).toContain("<b>Bold</b>")
|
|
2380
|
+
expect(html).toContain("<i>Italic</i>")
|
|
2381
|
+
expect(html).toContain("<u>Under</u>")
|
|
2382
|
+
expect(html).toContain("<s>Struck</s>")
|
|
2452
2383
|
})
|
|
2453
2384
|
|
|
2454
|
-
it(
|
|
2385
|
+
it("renders link as <a>", async () => {
|
|
2455
2386
|
const doc = Document({
|
|
2456
|
-
children: Link({ href:
|
|
2387
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2457
2388
|
})
|
|
2458
|
-
const html = (await render(doc,
|
|
2389
|
+
const html = (await render(doc, "telegram")) as string
|
|
2459
2390
|
expect(html).toContain('<a href="https://x.com">X</a>')
|
|
2460
2391
|
})
|
|
2461
2392
|
|
|
2462
|
-
it(
|
|
2393
|
+
it("renders table as pre-formatted text", async () => {
|
|
2463
2394
|
const doc = Document({
|
|
2464
|
-
children: Table({ columns: [
|
|
2395
|
+
children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
|
|
2465
2396
|
})
|
|
2466
|
-
const html = (await render(doc,
|
|
2467
|
-
expect(html).toContain(
|
|
2468
|
-
expect(html).toContain(
|
|
2469
|
-
expect(html).toContain(
|
|
2397
|
+
const html = (await render(doc, "telegram")) as string
|
|
2398
|
+
expect(html).toContain("<pre>")
|
|
2399
|
+
expect(html).toContain("A | B")
|
|
2400
|
+
expect(html).toContain("1 | 2")
|
|
2470
2401
|
})
|
|
2471
2402
|
|
|
2472
|
-
it(
|
|
2403
|
+
it("renders code with language", async () => {
|
|
2473
2404
|
const doc = Document({
|
|
2474
|
-
children: Code({ language:
|
|
2405
|
+
children: Code({ language: "python", children: "x = 1" }),
|
|
2475
2406
|
})
|
|
2476
|
-
const html = (await render(doc,
|
|
2477
|
-
expect(html).toContain(
|
|
2478
|
-
expect(html).toContain(
|
|
2407
|
+
const html = (await render(doc, "telegram")) as string
|
|
2408
|
+
expect(html).toContain("language-python")
|
|
2409
|
+
expect(html).toContain("x = 1")
|
|
2479
2410
|
})
|
|
2480
2411
|
|
|
2481
|
-
it(
|
|
2482
|
-
const doc = Document({ children: Code({ children:
|
|
2483
|
-
const html = (await render(doc,
|
|
2484
|
-
expect(html).toContain(
|
|
2412
|
+
it("renders code without language", async () => {
|
|
2413
|
+
const doc = Document({ children: Code({ children: "x = 1" }) })
|
|
2414
|
+
const html = (await render(doc, "telegram")) as string
|
|
2415
|
+
expect(html).toContain("<pre>x = 1</pre>")
|
|
2485
2416
|
})
|
|
2486
2417
|
|
|
2487
|
-
it(
|
|
2488
|
-
const doc = Document({ children: Quote({ children:
|
|
2489
|
-
const html = (await render(doc,
|
|
2490
|
-
expect(html).toContain(
|
|
2418
|
+
it("renders quote as blockquote", async () => {
|
|
2419
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2420
|
+
const html = (await render(doc, "telegram")) as string
|
|
2421
|
+
expect(html).toContain("<blockquote>wise</blockquote>")
|
|
2491
2422
|
})
|
|
2492
2423
|
|
|
2493
|
-
it(
|
|
2424
|
+
it("renders list", async () => {
|
|
2494
2425
|
const doc = Document({
|
|
2495
2426
|
children: List({
|
|
2496
|
-
children: [
|
|
2497
|
-
ListItem({ children: 'one' }),
|
|
2498
|
-
ListItem({ children: 'two' }),
|
|
2499
|
-
],
|
|
2427
|
+
children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
|
|
2500
2428
|
}),
|
|
2501
2429
|
})
|
|
2502
|
-
const html = (await render(doc,
|
|
2503
|
-
expect(html).toContain(
|
|
2504
|
-
expect(html).toContain(
|
|
2430
|
+
const html = (await render(doc, "telegram")) as string
|
|
2431
|
+
expect(html).toContain("• one")
|
|
2432
|
+
expect(html).toContain("• two")
|
|
2505
2433
|
})
|
|
2506
2434
|
|
|
2507
|
-
it(
|
|
2435
|
+
it("renders button as link", async () => {
|
|
2508
2436
|
const doc = Document({
|
|
2509
|
-
children: Button({ href:
|
|
2437
|
+
children: Button({ href: "/pay", children: "Pay" }),
|
|
2510
2438
|
})
|
|
2511
|
-
const html = (await render(doc,
|
|
2439
|
+
const html = (await render(doc, "telegram")) as string
|
|
2512
2440
|
expect(html).toContain('<a href="/pay">Pay</a>')
|
|
2513
2441
|
})
|
|
2514
2442
|
|
|
2515
|
-
it(
|
|
2516
|
-
const doc = Document({ children: Image({ src:
|
|
2517
|
-
const html = (await render(doc,
|
|
2518
|
-
expect(html).toBe(
|
|
2443
|
+
it("skips images (sent separately in Telegram)", async () => {
|
|
2444
|
+
const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
|
|
2445
|
+
const html = (await render(doc, "telegram")) as string
|
|
2446
|
+
expect(html).toBe("")
|
|
2519
2447
|
})
|
|
2520
2448
|
|
|
2521
|
-
it(
|
|
2449
|
+
it("escapes HTML entities", async () => {
|
|
2522
2450
|
const doc = Document({
|
|
2523
|
-
children: Text({ children:
|
|
2451
|
+
children: Text({ children: "<script>alert(1)</script>" }),
|
|
2524
2452
|
})
|
|
2525
|
-
const html = (await render(doc,
|
|
2526
|
-
expect(html).not.toContain(
|
|
2527
|
-
expect(html).toContain(
|
|
2453
|
+
const html = (await render(doc, "telegram")) as string
|
|
2454
|
+
expect(html).not.toContain("<script>")
|
|
2455
|
+
expect(html).toContain("<script>")
|
|
2528
2456
|
})
|
|
2529
2457
|
|
|
2530
|
-
it(
|
|
2531
|
-
const html = await createDocument().heading(
|
|
2532
|
-
expect(html).toContain(
|
|
2533
|
-
expect(html).toContain(
|
|
2458
|
+
it("builder toTelegram works", async () => {
|
|
2459
|
+
const html = await createDocument().heading("Hi").text("World").toTelegram()
|
|
2460
|
+
expect(html).toContain("<b>Hi</b>")
|
|
2461
|
+
expect(html).toContain("World")
|
|
2534
2462
|
})
|
|
2535
2463
|
})
|
|
2536
2464
|
|
|
2537
2465
|
// ─── Notion Renderer ────────────────────────────────────────────────────────
|
|
2538
2466
|
|
|
2539
|
-
describe(
|
|
2540
|
-
it(
|
|
2541
|
-
const doc = Document({ children: Heading({ children:
|
|
2542
|
-
const json = (await render(doc,
|
|
2467
|
+
describe("Notion renderer", () => {
|
|
2468
|
+
it("renders heading as heading block", async () => {
|
|
2469
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2470
|
+
const json = (await render(doc, "notion")) as string
|
|
2543
2471
|
const parsed = JSON.parse(json)
|
|
2544
|
-
expect(parsed.children[0].type).toBe(
|
|
2472
|
+
expect(parsed.children[0].type).toBe("heading_1")
|
|
2545
2473
|
})
|
|
2546
2474
|
|
|
2547
|
-
it(
|
|
2548
|
-
const doc = Document({ children: Heading({ level: 2, children:
|
|
2549
|
-
const json = (await render(doc,
|
|
2475
|
+
it("renders h2 as heading_2", async () => {
|
|
2476
|
+
const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
|
|
2477
|
+
const json = (await render(doc, "notion")) as string
|
|
2550
2478
|
const parsed = JSON.parse(json)
|
|
2551
|
-
expect(parsed.children[0].type).toBe(
|
|
2479
|
+
expect(parsed.children[0].type).toBe("heading_2")
|
|
2552
2480
|
})
|
|
2553
2481
|
|
|
2554
|
-
it(
|
|
2555
|
-
const doc = Document({ children: Heading({ level: 4, children:
|
|
2556
|
-
const json = (await render(doc,
|
|
2482
|
+
it("renders h3+ as heading_3", async () => {
|
|
2483
|
+
const doc = Document({ children: Heading({ level: 4, children: "Sub" }) })
|
|
2484
|
+
const json = (await render(doc, "notion")) as string
|
|
2557
2485
|
const parsed = JSON.parse(json)
|
|
2558
|
-
expect(parsed.children[0].type).toBe(
|
|
2486
|
+
expect(parsed.children[0].type).toBe("heading_3")
|
|
2559
2487
|
})
|
|
2560
2488
|
|
|
2561
|
-
it(
|
|
2562
|
-
const doc = Document({ children: Text({ bold: true, children:
|
|
2563
|
-
const json = (await render(doc,
|
|
2489
|
+
it("renders text as paragraph", async () => {
|
|
2490
|
+
const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
|
|
2491
|
+
const json = (await render(doc, "notion")) as string
|
|
2564
2492
|
const parsed = JSON.parse(json)
|
|
2565
|
-
expect(parsed.children[0].type).toBe(
|
|
2566
|
-
expect(parsed.children[0].paragraph.rich_text[0].annotations.bold).toBe(
|
|
2567
|
-
true,
|
|
2568
|
-
)
|
|
2493
|
+
expect(parsed.children[0].type).toBe("paragraph")
|
|
2494
|
+
expect(parsed.children[0].paragraph.rich_text[0].annotations.bold).toBe(true)
|
|
2569
2495
|
})
|
|
2570
2496
|
|
|
2571
|
-
it(
|
|
2497
|
+
it("renders table with header row", async () => {
|
|
2572
2498
|
const doc = Document({
|
|
2573
|
-
children: Table({ columns: [
|
|
2499
|
+
children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
|
|
2574
2500
|
})
|
|
2575
|
-
const json = (await render(doc,
|
|
2501
|
+
const json = (await render(doc, "notion")) as string
|
|
2576
2502
|
const parsed = JSON.parse(json)
|
|
2577
|
-
expect(parsed.children[0].type).toBe(
|
|
2503
|
+
expect(parsed.children[0].type).toBe("table")
|
|
2578
2504
|
expect(parsed.children[0].table.has_column_header).toBe(true)
|
|
2579
2505
|
})
|
|
2580
2506
|
|
|
2581
|
-
it(
|
|
2507
|
+
it("renders bulleted list", async () => {
|
|
2582
2508
|
const doc = Document({
|
|
2583
|
-
children: List({ children: [ListItem({ children:
|
|
2509
|
+
children: List({ children: [ListItem({ children: "a" })] }),
|
|
2584
2510
|
})
|
|
2585
|
-
const json = (await render(doc,
|
|
2511
|
+
const json = (await render(doc, "notion")) as string
|
|
2586
2512
|
const parsed = JSON.parse(json)
|
|
2587
|
-
expect(parsed.children[0].type).toBe(
|
|
2513
|
+
expect(parsed.children[0].type).toBe("bulleted_list_item")
|
|
2588
2514
|
})
|
|
2589
2515
|
|
|
2590
|
-
it(
|
|
2516
|
+
it("renders numbered list", async () => {
|
|
2591
2517
|
const doc = Document({
|
|
2592
2518
|
children: List({
|
|
2593
2519
|
ordered: true,
|
|
2594
|
-
children: [ListItem({ children:
|
|
2520
|
+
children: [ListItem({ children: "a" })],
|
|
2595
2521
|
}),
|
|
2596
2522
|
})
|
|
2597
|
-
const json = (await render(doc,
|
|
2523
|
+
const json = (await render(doc, "notion")) as string
|
|
2598
2524
|
const parsed = JSON.parse(json)
|
|
2599
|
-
expect(parsed.children[0].type).toBe(
|
|
2525
|
+
expect(parsed.children[0].type).toBe("numbered_list_item")
|
|
2600
2526
|
})
|
|
2601
2527
|
|
|
2602
|
-
it(
|
|
2528
|
+
it("renders code block", async () => {
|
|
2603
2529
|
const doc = Document({
|
|
2604
|
-
children: Code({ language:
|
|
2530
|
+
children: Code({ language: "python", children: "x = 1" }),
|
|
2605
2531
|
})
|
|
2606
|
-
const json = (await render(doc,
|
|
2532
|
+
const json = (await render(doc, "notion")) as string
|
|
2607
2533
|
const parsed = JSON.parse(json)
|
|
2608
|
-
expect(parsed.children[0].type).toBe(
|
|
2609
|
-
expect(parsed.children[0].code.language).toBe(
|
|
2534
|
+
expect(parsed.children[0].type).toBe("code")
|
|
2535
|
+
expect(parsed.children[0].code.language).toBe("python")
|
|
2610
2536
|
})
|
|
2611
2537
|
|
|
2612
|
-
it(
|
|
2613
|
-
const doc = Document({ children: Quote({ children:
|
|
2614
|
-
const json = (await render(doc,
|
|
2538
|
+
it("renders quote", async () => {
|
|
2539
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2540
|
+
const json = (await render(doc, "notion")) as string
|
|
2615
2541
|
const parsed = JSON.parse(json)
|
|
2616
|
-
expect(parsed.children[0].type).toBe(
|
|
2542
|
+
expect(parsed.children[0].type).toBe("quote")
|
|
2617
2543
|
})
|
|
2618
2544
|
|
|
2619
|
-
it(
|
|
2545
|
+
it("renders divider", async () => {
|
|
2620
2546
|
const doc = Document({ children: Divider() })
|
|
2621
|
-
const json = (await render(doc,
|
|
2547
|
+
const json = (await render(doc, "notion")) as string
|
|
2622
2548
|
const parsed = JSON.parse(json)
|
|
2623
|
-
expect(parsed.children[0].type).toBe(
|
|
2549
|
+
expect(parsed.children[0].type).toBe("divider")
|
|
2624
2550
|
})
|
|
2625
2551
|
|
|
2626
|
-
it(
|
|
2627
|
-
const doc = Document({ children: Image({ src:
|
|
2628
|
-
const json = (await render(doc,
|
|
2552
|
+
it("renders image with URL", async () => {
|
|
2553
|
+
const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
|
|
2554
|
+
const json = (await render(doc, "notion")) as string
|
|
2629
2555
|
const parsed = JSON.parse(json)
|
|
2630
|
-
expect(parsed.children[0].type).toBe(
|
|
2556
|
+
expect(parsed.children[0].type).toBe("image")
|
|
2631
2557
|
})
|
|
2632
2558
|
|
|
2633
|
-
it(
|
|
2559
|
+
it("renders link as paragraph with link", async () => {
|
|
2634
2560
|
const doc = Document({
|
|
2635
|
-
children: Link({ href:
|
|
2561
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2636
2562
|
})
|
|
2637
|
-
const json = (await render(doc,
|
|
2563
|
+
const json = (await render(doc, "notion")) as string
|
|
2638
2564
|
const parsed = JSON.parse(json)
|
|
2639
|
-
expect(parsed.children[0].paragraph.rich_text[0].text.link.url).toBe(
|
|
2640
|
-
'https://x.com',
|
|
2641
|
-
)
|
|
2565
|
+
expect(parsed.children[0].paragraph.rich_text[0].text.link.url).toBe("https://x.com")
|
|
2642
2566
|
})
|
|
2643
2567
|
|
|
2644
|
-
it(
|
|
2645
|
-
const json = await createDocument().heading(
|
|
2568
|
+
it("builder toNotion works", async () => {
|
|
2569
|
+
const json = await createDocument().heading("Hi").toNotion()
|
|
2646
2570
|
const parsed = JSON.parse(json)
|
|
2647
2571
|
expect(parsed.children.length).toBeGreaterThan(0)
|
|
2648
2572
|
})
|
|
@@ -2650,271 +2574,299 @@ describe('Notion renderer', () => {
|
|
|
2650
2574
|
|
|
2651
2575
|
// ─── Confluence/Jira Renderer ───────────────────────────────────────────────
|
|
2652
2576
|
|
|
2653
|
-
describe(
|
|
2654
|
-
it(
|
|
2655
|
-
const doc = Document({ children: Heading({ children:
|
|
2656
|
-
const json = (await render(doc,
|
|
2577
|
+
describe("Confluence renderer", () => {
|
|
2578
|
+
it("renders ADF document", async () => {
|
|
2579
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2580
|
+
const json = (await render(doc, "confluence")) as string
|
|
2657
2581
|
const adf = JSON.parse(json)
|
|
2658
2582
|
expect(adf.version).toBe(1)
|
|
2659
|
-
expect(adf.type).toBe(
|
|
2660
|
-
expect(adf.content[0].type).toBe(
|
|
2583
|
+
expect(adf.type).toBe("doc")
|
|
2584
|
+
expect(adf.content[0].type).toBe("heading")
|
|
2661
2585
|
})
|
|
2662
2586
|
|
|
2663
|
-
it(
|
|
2587
|
+
it("renders text with marks", async () => {
|
|
2664
2588
|
const doc = Document({
|
|
2665
|
-
children: Text({ bold: true, italic: true, children:
|
|
2589
|
+
children: Text({ bold: true, italic: true, children: "styled" }),
|
|
2666
2590
|
})
|
|
2667
|
-
const json = (await render(doc,
|
|
2591
|
+
const json = (await render(doc, "confluence")) as string
|
|
2668
2592
|
const adf = JSON.parse(json)
|
|
2669
2593
|
const marks = adf.content[0].content[0].marks
|
|
2670
|
-
expect(marks.some((m: any) => m.type ===
|
|
2671
|
-
expect(marks.some((m: any) => m.type ===
|
|
2594
|
+
expect(marks.some((m: any) => m.type === "strong")).toBe(true)
|
|
2595
|
+
expect(marks.some((m: any) => m.type === "em")).toBe(true)
|
|
2672
2596
|
})
|
|
2673
2597
|
|
|
2674
|
-
it(
|
|
2598
|
+
it("renders table", async () => {
|
|
2675
2599
|
const doc = Document({
|
|
2676
|
-
children: Table({ columns: [
|
|
2600
|
+
children: Table({ columns: ["A"], rows: [["1"]] }),
|
|
2677
2601
|
})
|
|
2678
|
-
const json = (await render(doc,
|
|
2602
|
+
const json = (await render(doc, "confluence")) as string
|
|
2679
2603
|
const adf = JSON.parse(json)
|
|
2680
|
-
expect(adf.content[0].type).toBe(
|
|
2681
|
-
expect(adf.content[0].content[0].content[0].type).toBe(
|
|
2604
|
+
expect(adf.content[0].type).toBe("table")
|
|
2605
|
+
expect(adf.content[0].content[0].content[0].type).toBe("tableHeader")
|
|
2682
2606
|
})
|
|
2683
2607
|
|
|
2684
|
-
it(
|
|
2608
|
+
it("renders ordered list", async () => {
|
|
2685
2609
|
const doc = Document({
|
|
2686
2610
|
children: List({
|
|
2687
2611
|
ordered: true,
|
|
2688
|
-
children: [ListItem({ children:
|
|
2612
|
+
children: [ListItem({ children: "a" })],
|
|
2689
2613
|
}),
|
|
2690
2614
|
})
|
|
2691
|
-
const json = (await render(doc,
|
|
2615
|
+
const json = (await render(doc, "confluence")) as string
|
|
2692
2616
|
const adf = JSON.parse(json)
|
|
2693
|
-
expect(adf.content[0].type).toBe(
|
|
2617
|
+
expect(adf.content[0].type).toBe("orderedList")
|
|
2694
2618
|
})
|
|
2695
2619
|
|
|
2696
|
-
it(
|
|
2620
|
+
it("renders code block", async () => {
|
|
2697
2621
|
const doc = Document({
|
|
2698
|
-
children: Code({ language:
|
|
2622
|
+
children: Code({ language: "java", children: "int x = 1;" }),
|
|
2699
2623
|
})
|
|
2700
|
-
const json = (await render(doc,
|
|
2624
|
+
const json = (await render(doc, "confluence")) as string
|
|
2701
2625
|
const adf = JSON.parse(json)
|
|
2702
|
-
expect(adf.content[0].type).toBe(
|
|
2703
|
-
expect(adf.content[0].attrs.language).toBe(
|
|
2626
|
+
expect(adf.content[0].type).toBe("codeBlock")
|
|
2627
|
+
expect(adf.content[0].attrs.language).toBe("java")
|
|
2704
2628
|
})
|
|
2705
2629
|
|
|
2706
|
-
it(
|
|
2707
|
-
const doc = Document({ children: Quote({ children:
|
|
2708
|
-
const json = (await render(doc,
|
|
2630
|
+
it("renders blockquote", async () => {
|
|
2631
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2632
|
+
const json = (await render(doc, "confluence")) as string
|
|
2709
2633
|
const adf = JSON.parse(json)
|
|
2710
|
-
expect(adf.content[0].type).toBe(
|
|
2634
|
+
expect(adf.content[0].type).toBe("blockquote")
|
|
2711
2635
|
})
|
|
2712
2636
|
|
|
2713
|
-
it(
|
|
2637
|
+
it("renders rule (divider)", async () => {
|
|
2714
2638
|
const doc = Document({ children: Divider() })
|
|
2715
|
-
const json = (await render(doc,
|
|
2639
|
+
const json = (await render(doc, "confluence")) as string
|
|
2716
2640
|
const adf = JSON.parse(json)
|
|
2717
|
-
expect(adf.content[0].type).toBe(
|
|
2641
|
+
expect(adf.content[0].type).toBe("rule")
|
|
2718
2642
|
})
|
|
2719
2643
|
|
|
2720
|
-
it(
|
|
2644
|
+
it("renders link with href", async () => {
|
|
2721
2645
|
const doc = Document({
|
|
2722
|
-
children: Link({ href:
|
|
2646
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2723
2647
|
})
|
|
2724
|
-
const json = (await render(doc,
|
|
2648
|
+
const json = (await render(doc, "confluence")) as string
|
|
2725
2649
|
const adf = JSON.parse(json)
|
|
2726
|
-
expect(adf.content[0].content[0].marks[0].attrs.href).toBe(
|
|
2650
|
+
expect(adf.content[0].content[0].marks[0].attrs.href).toBe("https://x.com")
|
|
2727
2651
|
})
|
|
2728
2652
|
|
|
2729
|
-
it(
|
|
2730
|
-
const json = await createDocument().heading(
|
|
2653
|
+
it("builder toConfluence works", async () => {
|
|
2654
|
+
const json = await createDocument().heading("Hi").toConfluence()
|
|
2731
2655
|
const adf = JSON.parse(json)
|
|
2732
|
-
expect(adf.type).toBe(
|
|
2656
|
+
expect(adf.type).toBe("doc")
|
|
2733
2657
|
})
|
|
2734
2658
|
})
|
|
2735
2659
|
|
|
2736
2660
|
// ─── WhatsApp Renderer ──────────────────────────────────────────────────────
|
|
2737
2661
|
|
|
2738
|
-
describe(
|
|
2739
|
-
it(
|
|
2740
|
-
const doc = Document({ children: Heading({ children:
|
|
2741
|
-
const text = (await render(doc,
|
|
2742
|
-
expect(text).toContain(
|
|
2662
|
+
describe("WhatsApp renderer", () => {
|
|
2663
|
+
it("renders heading as bold", async () => {
|
|
2664
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2665
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2666
|
+
expect(text).toContain("*Title*")
|
|
2743
2667
|
})
|
|
2744
2668
|
|
|
2745
|
-
it(
|
|
2669
|
+
it("renders bold, italic, strikethrough", async () => {
|
|
2746
2670
|
const doc = Document({
|
|
2747
2671
|
children: [
|
|
2748
|
-
Text({ bold: true, children:
|
|
2749
|
-
Text({ italic: true, children:
|
|
2750
|
-
Text({ strikethrough: true, children:
|
|
2672
|
+
Text({ bold: true, children: "Bold" }),
|
|
2673
|
+
Text({ italic: true, children: "Italic" }),
|
|
2674
|
+
Text({ strikethrough: true, children: "Struck" }),
|
|
2751
2675
|
],
|
|
2752
2676
|
})
|
|
2753
|
-
const text = (await render(doc,
|
|
2754
|
-
expect(text).toContain(
|
|
2755
|
-
expect(text).toContain(
|
|
2756
|
-
expect(text).toContain(
|
|
2677
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2678
|
+
expect(text).toContain("*Bold*")
|
|
2679
|
+
expect(text).toContain("_Italic_")
|
|
2680
|
+
expect(text).toContain("~Struck~")
|
|
2757
2681
|
})
|
|
2758
2682
|
|
|
2759
|
-
it(
|
|
2760
|
-
const doc = Document({ children: Code({ children:
|
|
2761
|
-
const text = (await render(doc,
|
|
2762
|
-
expect(text).toContain(
|
|
2683
|
+
it("renders code as triple backticks", async () => {
|
|
2684
|
+
const doc = Document({ children: Code({ children: "x = 1" }) })
|
|
2685
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2686
|
+
expect(text).toContain("```x = 1```")
|
|
2763
2687
|
})
|
|
2764
2688
|
|
|
2765
|
-
it(
|
|
2766
|
-
const doc = Document({ children: Quote({ children:
|
|
2767
|
-
const text = (await render(doc,
|
|
2768
|
-
expect(text).toContain(
|
|
2689
|
+
it("renders quote with >", async () => {
|
|
2690
|
+
const doc = Document({ children: Quote({ children: "wise" }) })
|
|
2691
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2692
|
+
expect(text).toContain("> wise")
|
|
2769
2693
|
})
|
|
2770
2694
|
|
|
2771
|
-
it(
|
|
2695
|
+
it("renders link as text + URL", async () => {
|
|
2772
2696
|
const doc = Document({
|
|
2773
|
-
children: Link({ href:
|
|
2697
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2774
2698
|
})
|
|
2775
|
-
const text = (await render(doc,
|
|
2776
|
-
expect(text).toContain(
|
|
2699
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2700
|
+
expect(text).toContain("X: https://x.com")
|
|
2777
2701
|
})
|
|
2778
2702
|
|
|
2779
|
-
it(
|
|
2703
|
+
it("renders table", async () => {
|
|
2780
2704
|
const doc = Document({
|
|
2781
|
-
children: Table({ columns: [
|
|
2705
|
+
children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
|
|
2782
2706
|
})
|
|
2783
|
-
const text = (await render(doc,
|
|
2784
|
-
expect(text).toContain(
|
|
2785
|
-
expect(text).toContain(
|
|
2707
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2708
|
+
expect(text).toContain("*A* | *B*")
|
|
2709
|
+
expect(text).toContain("1 | 2")
|
|
2786
2710
|
})
|
|
2787
2711
|
|
|
2788
|
-
it(
|
|
2712
|
+
it("renders list", async () => {
|
|
2789
2713
|
const doc = Document({
|
|
2790
|
-
children: List({ children: [ListItem({ children:
|
|
2714
|
+
children: List({ children: [ListItem({ children: "one" })] }),
|
|
2791
2715
|
})
|
|
2792
|
-
const text = (await render(doc,
|
|
2793
|
-
expect(text).toContain(
|
|
2716
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2717
|
+
expect(text).toContain("• one")
|
|
2794
2718
|
})
|
|
2795
2719
|
|
|
2796
|
-
it(
|
|
2797
|
-
const doc = Document({ children: Image({ src:
|
|
2798
|
-
const text = (await render(doc,
|
|
2799
|
-
expect(text).toBe(
|
|
2720
|
+
it("skips images", async () => {
|
|
2721
|
+
const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
|
|
2722
|
+
const text = (await render(doc, "whatsapp")) as string
|
|
2723
|
+
expect(text).toBe("")
|
|
2800
2724
|
})
|
|
2801
2725
|
|
|
2802
|
-
it(
|
|
2803
|
-
const text = await createDocument().heading(
|
|
2804
|
-
expect(text).toContain(
|
|
2805
|
-
expect(text).toContain(
|
|
2726
|
+
it("builder toWhatsApp works", async () => {
|
|
2727
|
+
const text = await createDocument().heading("Hi").text("World").toWhatsApp()
|
|
2728
|
+
expect(text).toContain("*Hi*")
|
|
2729
|
+
expect(text).toContain("World")
|
|
2806
2730
|
})
|
|
2807
2731
|
})
|
|
2808
2732
|
|
|
2809
2733
|
// ─── Google Chat Renderer ───────────────────────────────────────────────────
|
|
2810
2734
|
|
|
2811
|
-
describe(
|
|
2812
|
-
it(
|
|
2735
|
+
describe("Google Chat renderer", () => {
|
|
2736
|
+
it("renders card with header", async () => {
|
|
2813
2737
|
const doc = Document({
|
|
2814
|
-
title:
|
|
2815
|
-
children: Text({ children:
|
|
2738
|
+
title: "Report",
|
|
2739
|
+
children: Text({ children: "Body" }),
|
|
2816
2740
|
})
|
|
2817
|
-
const json = (await render(doc,
|
|
2741
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2818
2742
|
const card = JSON.parse(json)
|
|
2819
|
-
expect(card.cardsV2[0].card.header.title).toBe(
|
|
2743
|
+
expect(card.cardsV2[0].card.header.title).toBe("Report")
|
|
2820
2744
|
})
|
|
2821
2745
|
|
|
2822
|
-
it(
|
|
2823
|
-
const doc = Document({ children: Heading({ children:
|
|
2824
|
-
const json = (await render(doc,
|
|
2746
|
+
it("renders heading as decorated text", async () => {
|
|
2747
|
+
const doc = Document({ children: Heading({ children: "Title" }) })
|
|
2748
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2825
2749
|
const card = JSON.parse(json)
|
|
2826
|
-
expect(
|
|
2827
|
-
card.cardsV2[0].card.sections[0].widgets[0].decoratedText.text,
|
|
2828
|
-
).toContain('<b>Title</b>')
|
|
2750
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].decoratedText.text).toContain("<b>Title</b>")
|
|
2829
2751
|
})
|
|
2830
2752
|
|
|
2831
|
-
it(
|
|
2832
|
-
const doc = Document({ children: Text({ bold: true, children:
|
|
2833
|
-
const json = (await render(doc,
|
|
2753
|
+
it("renders text paragraph", async () => {
|
|
2754
|
+
const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
|
|
2755
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2834
2756
|
const card = JSON.parse(json)
|
|
2835
|
-
expect(
|
|
2836
|
-
card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text,
|
|
2837
|
-
).toContain('<b>Bold</b>')
|
|
2757
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain("<b>Bold</b>")
|
|
2838
2758
|
})
|
|
2839
2759
|
|
|
2840
|
-
it(
|
|
2760
|
+
it("renders button", async () => {
|
|
2841
2761
|
const doc = Document({
|
|
2842
|
-
children: Button({ href:
|
|
2762
|
+
children: Button({ href: "/go", children: "Click" }),
|
|
2843
2763
|
})
|
|
2844
|
-
const json = (await render(doc,
|
|
2764
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2845
2765
|
const card = JSON.parse(json)
|
|
2846
|
-
expect(
|
|
2847
|
-
card.cardsV2[0].card.sections[0].widgets[0].buttonList.buttons[0].text,
|
|
2848
|
-
).toBe('Click')
|
|
2766
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].buttonList.buttons[0].text).toBe("Click")
|
|
2849
2767
|
})
|
|
2850
2768
|
|
|
2851
|
-
it(
|
|
2769
|
+
it("renders divider", async () => {
|
|
2852
2770
|
const doc = Document({ children: Divider() })
|
|
2853
|
-
const json = (await render(doc,
|
|
2771
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2854
2772
|
const card = JSON.parse(json)
|
|
2855
2773
|
expect(card.cardsV2[0].card.sections[0].widgets[0].divider).toBeDefined()
|
|
2856
2774
|
})
|
|
2857
2775
|
|
|
2858
|
-
it(
|
|
2776
|
+
it("renders image", async () => {
|
|
2859
2777
|
const doc = Document({
|
|
2860
|
-
children: Image({ src:
|
|
2778
|
+
children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
|
|
2861
2779
|
})
|
|
2862
|
-
const json = (await render(doc,
|
|
2780
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2863
2781
|
const card = JSON.parse(json)
|
|
2864
|
-
expect(card.cardsV2[0].card.sections[0].widgets[0].image.imageUrl).toBe(
|
|
2865
|
-
'https://x.com/img.png',
|
|
2866
|
-
)
|
|
2782
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].image.imageUrl).toBe("https://x.com/img.png")
|
|
2867
2783
|
})
|
|
2868
2784
|
|
|
2869
|
-
it(
|
|
2785
|
+
it("renders link", async () => {
|
|
2870
2786
|
const doc = Document({
|
|
2871
|
-
children: Link({ href:
|
|
2787
|
+
children: Link({ href: "https://x.com", children: "X" }),
|
|
2872
2788
|
})
|
|
2873
|
-
const json = (await render(doc,
|
|
2789
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2874
2790
|
const card = JSON.parse(json)
|
|
2875
|
-
expect(
|
|
2876
|
-
|
|
2877
|
-
)
|
|
2791
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain(
|
|
2792
|
+
'href="https://x.com"',
|
|
2793
|
+
)
|
|
2878
2794
|
})
|
|
2879
2795
|
|
|
2880
|
-
it(
|
|
2796
|
+
it("renders list", async () => {
|
|
2881
2797
|
const doc = Document({
|
|
2882
|
-
children: List({ children: [ListItem({ children:
|
|
2798
|
+
children: List({ children: [ListItem({ children: "one" })] }),
|
|
2883
2799
|
})
|
|
2884
|
-
const json = (await render(doc,
|
|
2800
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2885
2801
|
const card = JSON.parse(json)
|
|
2886
|
-
expect(
|
|
2887
|
-
card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text,
|
|
2888
|
-
).toContain('• one')
|
|
2802
|
+
expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain("• one")
|
|
2889
2803
|
})
|
|
2890
2804
|
|
|
2891
|
-
it(
|
|
2805
|
+
it("uses first heading as title when no title prop", async () => {
|
|
2892
2806
|
const doc = Document({
|
|
2893
|
-
children: [
|
|
2894
|
-
Heading({ children: 'Auto Title' }),
|
|
2895
|
-
Text({ children: 'body' }),
|
|
2896
|
-
],
|
|
2807
|
+
children: [Heading({ children: "Auto Title" }), Text({ children: "body" })],
|
|
2897
2808
|
})
|
|
2898
|
-
const json = (await render(doc,
|
|
2809
|
+
const json = (await render(doc, "google-chat")) as string
|
|
2899
2810
|
const card = JSON.parse(json)
|
|
2900
|
-
expect(card.cardsV2[0].card.header.title).toBe(
|
|
2811
|
+
expect(card.cardsV2[0].card.header.title).toBe("Auto Title")
|
|
2901
2812
|
})
|
|
2902
2813
|
|
|
2903
|
-
it(
|
|
2904
|
-
const json = await createDocument({ title:
|
|
2905
|
-
.text('World')
|
|
2906
|
-
.toGoogleChat()
|
|
2814
|
+
it("builder toGoogleChat works", async () => {
|
|
2815
|
+
const json = await createDocument({ title: "Hi" }).text("World").toGoogleChat()
|
|
2907
2816
|
const card = JSON.parse(json)
|
|
2908
|
-
expect(card.cardsV2[0].card.header.title).toBe(
|
|
2817
|
+
expect(card.cardsV2[0].card.header.title).toBe("Hi")
|
|
2909
2818
|
})
|
|
2910
2819
|
})
|
|
2911
2820
|
|
|
2912
|
-
describe(
|
|
2913
|
-
it(
|
|
2914
|
-
const result = await createDocument().heading(
|
|
2821
|
+
describe("builder toSlack", () => {
|
|
2822
|
+
it("renders to Slack JSON", async () => {
|
|
2823
|
+
const result = await createDocument().heading("Hi").text("World").toSlack()
|
|
2915
2824
|
const parsed = JSON.parse(result)
|
|
2916
2825
|
expect(parsed.blocks).toHaveLength(2)
|
|
2917
|
-
expect(parsed.blocks[0].type).toBe(
|
|
2918
|
-
expect(parsed.blocks[1].type).toBe(
|
|
2826
|
+
expect(parsed.blocks[0].type).toBe("header")
|
|
2827
|
+
expect(parsed.blocks[1].type).toBe("section")
|
|
2828
|
+
})
|
|
2829
|
+
})
|
|
2830
|
+
|
|
2831
|
+
// ─── Builder .add() and .section() ──────────────────────────────────────────
|
|
2832
|
+
|
|
2833
|
+
describe("builder .add() and .section()", () => {
|
|
2834
|
+
// build() wraps sections in Document({ children: [Page({ children: sections })] })
|
|
2835
|
+
// so actual content nodes are inside the page's children
|
|
2836
|
+
function getPageChildren(doc: ReturnType<typeof createDocument>) {
|
|
2837
|
+
const node = doc.build()
|
|
2838
|
+
expect(node.type).toBe("document")
|
|
2839
|
+
const page = node.children[0]!
|
|
2840
|
+
expect(typeof page !== "string" && page.type).toBe("page")
|
|
2841
|
+
return typeof page !== "string" ? page.children : []
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
it("add() with a single node adds it to the document", () => {
|
|
2845
|
+
const doc = createDocument().add(Text({ children: "hello" }))
|
|
2846
|
+
const children = getPageChildren(doc)
|
|
2847
|
+
expect(children).toHaveLength(1)
|
|
2848
|
+
const child = children[0]!
|
|
2849
|
+
expect(typeof child !== "string" && child.type).toBe("text")
|
|
2850
|
+
})
|
|
2851
|
+
|
|
2852
|
+
it("add() with an array adds multiple nodes", () => {
|
|
2853
|
+
const doc = createDocument().add([Heading({ children: "a" }), Text({ children: "b" })])
|
|
2854
|
+
const children = getPageChildren(doc)
|
|
2855
|
+
expect(children).toHaveLength(2)
|
|
2856
|
+
const first = children[0]!
|
|
2857
|
+
const second = children[1]!
|
|
2858
|
+
expect(typeof first !== "string" && first.type).toBe("heading")
|
|
2859
|
+
expect(typeof second !== "string" && second.type).toBe("text")
|
|
2860
|
+
})
|
|
2861
|
+
|
|
2862
|
+
it("section() wraps children in a Section node", () => {
|
|
2863
|
+
const doc = createDocument().section([Text({ children: "a" }), Text({ children: "b" })])
|
|
2864
|
+
const children = getPageChildren(doc)
|
|
2865
|
+
expect(children).toHaveLength(1)
|
|
2866
|
+
const section = children[0]!
|
|
2867
|
+
expect(typeof section !== "string" && section.type).toBe("section")
|
|
2868
|
+
if (typeof section !== "string") {
|
|
2869
|
+
expect(section.children).toHaveLength(2)
|
|
2870
|
+
}
|
|
2919
2871
|
})
|
|
2920
2872
|
})
|