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