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