@startsimpli/ui 0.4.7 → 0.4.8
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/package.json +1 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +183 -200
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/index.ts +6 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +6 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/editor-sidebar.tsx +6 -731
- package/src/components/email-editor/email-editor.tsx +78 -467
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +1 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +134 -881
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { renderBlockToHtml } from '../renderer/block-renderers'
|
|
2
|
+
import type {
|
|
3
|
+
TextBlock,
|
|
4
|
+
MetricsBlock,
|
|
5
|
+
DividerBlock,
|
|
6
|
+
CTABlock,
|
|
7
|
+
ImageBlock,
|
|
8
|
+
SpacerBlock,
|
|
9
|
+
SocialBlock,
|
|
10
|
+
HeaderBlock,
|
|
11
|
+
FooterBlock,
|
|
12
|
+
GlobalStyles,
|
|
13
|
+
Block,
|
|
14
|
+
} from '../types'
|
|
15
|
+
import { DEFAULT_GLOBAL_STYLES } from '../types'
|
|
16
|
+
|
|
17
|
+
const styles: GlobalStyles = DEFAULT_GLOBAL_STYLES
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// TextBlock
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
describe('renderBlockToHtml — text block', () => {
|
|
24
|
+
const base: TextBlock = {
|
|
25
|
+
id: 't1',
|
|
26
|
+
type: 'text',
|
|
27
|
+
content: '<p>Hello</p>',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('includes the text content in output', () => {
|
|
31
|
+
expect(renderBlockToHtml(base, styles)).toContain('<p>Hello</p>')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('applies default font-size 16px when not specified', () => {
|
|
35
|
+
expect(renderBlockToHtml(base, styles)).toContain('font-size:16px')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('uses block fontSize when provided', () => {
|
|
39
|
+
const html = renderBlockToHtml({ ...base, fontSize: 20 }, styles)
|
|
40
|
+
expect(html).toContain('font-size:20px')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('applies default line-height 1.6 when not specified', () => {
|
|
44
|
+
expect(renderBlockToHtml(base, styles)).toContain('line-height:1.6')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('uses block lineHeight when provided', () => {
|
|
48
|
+
const html = renderBlockToHtml({ ...base, lineHeight: 2 }, styles)
|
|
49
|
+
expect(html).toContain('line-height:2')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('applies default text color #1f2937 when not specified', () => {
|
|
53
|
+
expect(renderBlockToHtml(base, styles)).toContain('color:#1f2937')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('uses block textColor when provided', () => {
|
|
57
|
+
const html = renderBlockToHtml({ ...base, textColor: '#ff0000' }, styles)
|
|
58
|
+
expect(html).toContain('color:#ff0000')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('uses block fontFamily when provided', () => {
|
|
62
|
+
const html = renderBlockToHtml({ ...base, fontFamily: 'Georgia, serif' }, styles)
|
|
63
|
+
expect(html).toContain('font-family:Georgia, serif')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('falls back to globalStyles fontFamily when block fontFamily not set', () => {
|
|
67
|
+
const html = renderBlockToHtml(base, { ...styles, fontFamily: 'Arial, sans-serif' })
|
|
68
|
+
expect(html).toContain('font-family:Arial, sans-serif')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('includes block style padding when set', () => {
|
|
72
|
+
const block: TextBlock = { ...base, style: { paddingTop: 10, paddingBottom: 12 } }
|
|
73
|
+
const html = renderBlockToHtml(block, styles)
|
|
74
|
+
expect(html).toContain('padding-top:10px')
|
|
75
|
+
expect(html).toContain('padding-bottom:12px')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('renders a div element', () => {
|
|
79
|
+
expect(renderBlockToHtml(base, styles)).toMatch(/^<div/)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// MetricsBlock
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
describe('renderBlockToHtml — metrics block', () => {
|
|
88
|
+
const base: MetricsBlock = {
|
|
89
|
+
id: 'met1',
|
|
90
|
+
type: 'metrics',
|
|
91
|
+
title: 'Key Numbers',
|
|
92
|
+
metrics: [
|
|
93
|
+
{ id: 'm1', label: 'Revenue', value: '$10k', change: '+12%', changeType: 'positive' },
|
|
94
|
+
{ id: 'm2', label: 'Churn', value: '2%', change: '-1%', changeType: 'negative' },
|
|
95
|
+
],
|
|
96
|
+
columns: 2,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
it('includes the section title in output', () => {
|
|
100
|
+
expect(renderBlockToHtml(base, styles)).toContain('Key Numbers')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('omits title element when title is undefined', () => {
|
|
104
|
+
const html = renderBlockToHtml({ ...base, title: undefined }, styles)
|
|
105
|
+
expect(html).not.toContain('Key Numbers')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('includes metric value', () => {
|
|
109
|
+
expect(renderBlockToHtml(base, styles)).toContain('$10k')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('includes metric label', () => {
|
|
113
|
+
expect(renderBlockToHtml(base, styles)).toContain('Revenue')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('includes positive change in green (#16a34a)', () => {
|
|
117
|
+
const html = renderBlockToHtml(base, styles)
|
|
118
|
+
expect(html).toContain('+12%')
|
|
119
|
+
expect(html).toContain('#16a34a')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('includes negative change in red (#dc2626)', () => {
|
|
123
|
+
const html = renderBlockToHtml(base, styles)
|
|
124
|
+
expect(html).toContain('-1%')
|
|
125
|
+
expect(html).toContain('#dc2626')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders table element for metric cells', () => {
|
|
129
|
+
expect(renderBlockToHtml(base, styles)).toContain('<table')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('renders multiple metrics', () => {
|
|
133
|
+
const html = renderBlockToHtml(base, styles)
|
|
134
|
+
expect(html).toContain('Revenue')
|
|
135
|
+
expect(html).toContain('Churn')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('does not render change element when metric has no change field', () => {
|
|
139
|
+
const block: MetricsBlock = {
|
|
140
|
+
...base,
|
|
141
|
+
metrics: [{ id: 'mx', label: 'Solo', value: '7' }],
|
|
142
|
+
}
|
|
143
|
+
const html = renderBlockToHtml(block, styles)
|
|
144
|
+
expect(html).toContain('Solo')
|
|
145
|
+
expect(html).not.toContain('undefined')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// DividerBlock
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
describe('renderBlockToHtml — divider block', () => {
|
|
154
|
+
const base: DividerBlock = {
|
|
155
|
+
id: 'd1',
|
|
156
|
+
type: 'divider',
|
|
157
|
+
dividerStyle: 'solid',
|
|
158
|
+
color: '#aabbcc',
|
|
159
|
+
thickness: 2,
|
|
160
|
+
width: 80,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
it('renders solid border-top style', () => {
|
|
164
|
+
const html = renderBlockToHtml(base, styles)
|
|
165
|
+
expect(html).toContain('solid')
|
|
166
|
+
expect(html).toContain('#aabbcc')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('applies thickness to border-top', () => {
|
|
170
|
+
const html = renderBlockToHtml(base, styles)
|
|
171
|
+
expect(html).toContain('2px')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('applies width percentage', () => {
|
|
175
|
+
const html = renderBlockToHtml(base, styles)
|
|
176
|
+
expect(html).toContain('width:80%')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('renders dashed border for dashed style', () => {
|
|
180
|
+
const html = renderBlockToHtml({ ...base, dividerStyle: 'dashed' }, styles)
|
|
181
|
+
expect(html).toContain('dashed')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('renders dotted border for dotted style', () => {
|
|
185
|
+
const html = renderBlockToHtml({ ...base, dividerStyle: 'dotted' }, styles)
|
|
186
|
+
expect(html).toContain('dotted')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('renders height div (no border) for space style', () => {
|
|
190
|
+
const html = renderBlockToHtml({ ...base, dividerStyle: 'space' }, styles)
|
|
191
|
+
expect(html).toContain('height:32px')
|
|
192
|
+
expect(html).not.toContain('border-top')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('defaults color to #d1d5db when not specified', () => {
|
|
196
|
+
const html = renderBlockToHtml({ ...base, color: undefined }, styles)
|
|
197
|
+
expect(html).toContain('#d1d5db')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('defaults thickness to 1 when not specified', () => {
|
|
201
|
+
const html = renderBlockToHtml({ ...base, thickness: undefined }, styles)
|
|
202
|
+
expect(html).toContain('1px')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('defaults width to 100 when not specified', () => {
|
|
206
|
+
const html = renderBlockToHtml({ ...base, width: undefined }, styles)
|
|
207
|
+
expect(html).toContain('width:100%')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('wraps in a table element', () => {
|
|
211
|
+
const html = renderBlockToHtml(base, styles)
|
|
212
|
+
expect(html).toContain('<table')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// CTABlock (button)
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
describe('renderBlockToHtml — cta block', () => {
|
|
221
|
+
const base: CTABlock = {
|
|
222
|
+
id: 'cta1',
|
|
223
|
+
type: 'cta',
|
|
224
|
+
text: 'Get Started',
|
|
225
|
+
url: 'https://example.com/start',
|
|
226
|
+
buttonColor: '#6d28d9',
|
|
227
|
+
textColor: '#fafafa',
|
|
228
|
+
borderRadius: 8,
|
|
229
|
+
paddingH: 20,
|
|
230
|
+
paddingV: 10,
|
|
231
|
+
alignment: 'center',
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it('includes button text', () => {
|
|
235
|
+
expect(renderBlockToHtml(base, styles)).toContain('Get Started')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('includes the button URL in anchor href', () => {
|
|
239
|
+
expect(renderBlockToHtml(base, styles)).toContain('https://example.com/start')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('applies button background color', () => {
|
|
243
|
+
expect(renderBlockToHtml(base, styles)).toContain('#6d28d9')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('applies text color', () => {
|
|
247
|
+
expect(renderBlockToHtml(base, styles)).toContain('#fafafa')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('applies border-radius', () => {
|
|
251
|
+
expect(renderBlockToHtml(base, styles)).toContain('border-radius:8px')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('applies horizontal padding', () => {
|
|
255
|
+
expect(renderBlockToHtml(base, styles)).toContain('padding:10px 20px')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('applies alignment for center', () => {
|
|
259
|
+
expect(renderBlockToHtml(base, styles)).toContain('align="center"')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('applies alignment for left', () => {
|
|
263
|
+
const html = renderBlockToHtml({ ...base, alignment: 'left' }, styles)
|
|
264
|
+
expect(html).toContain('align="left"')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('applies alignment for right', () => {
|
|
268
|
+
const html = renderBlockToHtml({ ...base, alignment: 'right' }, styles)
|
|
269
|
+
expect(html).toContain('align="right"')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('defaults button color to #2563eb when not provided', () => {
|
|
273
|
+
const html = renderBlockToHtml({ ...base, buttonColor: undefined }, styles)
|
|
274
|
+
expect(html).toContain('#2563eb')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('defaults text color to #ffffff when not provided', () => {
|
|
278
|
+
const html = renderBlockToHtml({ ...base, textColor: undefined }, styles)
|
|
279
|
+
expect(html).toContain('#ffffff')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('falls back to # for URL when url is empty', () => {
|
|
283
|
+
const html = renderBlockToHtml({ ...base, url: '' }, styles)
|
|
284
|
+
expect(html).toContain('href="#"')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('includes MSO VML conditional comment', () => {
|
|
288
|
+
expect(renderBlockToHtml(base, styles)).toContain('[if mso]')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('includes [if !mso] conditional for non-Outlook clients', () => {
|
|
292
|
+
expect(renderBlockToHtml(base, styles)).toContain('[if !mso]')
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// ImageBlock
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
describe('renderBlockToHtml — image block', () => {
|
|
301
|
+
const base: ImageBlock = {
|
|
302
|
+
id: 'img1',
|
|
303
|
+
type: 'image',
|
|
304
|
+
url: 'https://img.example.com/photo.jpg',
|
|
305
|
+
alt: 'Photo description',
|
|
306
|
+
alignment: 'center',
|
|
307
|
+
width: 100,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
it('returns empty string when url is empty', () => {
|
|
311
|
+
expect(renderBlockToHtml({ ...base, url: '' }, styles)).toBe('')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('includes img src', () => {
|
|
315
|
+
expect(renderBlockToHtml(base, styles)).toContain('src="https://img.example.com/photo.jpg"')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('includes alt text', () => {
|
|
319
|
+
expect(renderBlockToHtml(base, styles)).toContain('alt="Photo description"')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('applies alignment via td align attribute', () => {
|
|
323
|
+
expect(renderBlockToHtml(base, styles)).toContain('align="center"')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('applies left alignment', () => {
|
|
327
|
+
const html = renderBlockToHtml({ ...base, alignment: 'left' }, styles)
|
|
328
|
+
expect(html).toContain('align="left"')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('applies width as pixel calculation', () => {
|
|
332
|
+
// 100% of 600 = 600
|
|
333
|
+
expect(renderBlockToHtml(base, styles)).toContain('width="600"')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('calculates partial width (50% of 600 = 300)', () => {
|
|
337
|
+
const html = renderBlockToHtml({ ...base, width: 50 }, styles)
|
|
338
|
+
expect(html).toContain('width="300"')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('wraps image in anchor when linkUrl is set', () => {
|
|
342
|
+
const html = renderBlockToHtml({ ...base, linkUrl: 'https://linked.example.com' }, styles)
|
|
343
|
+
expect(html).toContain('<a href="https://linked.example.com"')
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('does not wrap image in anchor when linkUrl is absent', () => {
|
|
347
|
+
const html = renderBlockToHtml(base, styles)
|
|
348
|
+
expect(html).not.toContain('<a href=')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('includes caption when provided', () => {
|
|
352
|
+
const html = renderBlockToHtml({ ...base, caption: 'Beautiful scenery' }, styles)
|
|
353
|
+
expect(html).toContain('Beautiful scenery')
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('does not include caption when absent', () => {
|
|
357
|
+
const html = renderBlockToHtml(base, styles)
|
|
358
|
+
expect(html).not.toContain('Beautiful scenery')
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// SpacerBlock
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
describe('renderBlockToHtml — spacer block', () => {
|
|
367
|
+
const base: SpacerBlock = {
|
|
368
|
+
id: 'sp1',
|
|
369
|
+
type: 'spacer',
|
|
370
|
+
height: 48,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
it('renders a height div', () => {
|
|
374
|
+
expect(renderBlockToHtml(base, styles)).toContain('height:48px')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('line-height matches height', () => {
|
|
378
|
+
expect(renderBlockToHtml(base, styles)).toContain('line-height:48px')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('defaults height to 32 when not provided', () => {
|
|
382
|
+
const html = renderBlockToHtml({ ...base, height: 0 }, styles)
|
|
383
|
+
expect(html).toContain('height:32px')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('renders a non-breaking space for email client support', () => {
|
|
387
|
+
expect(renderBlockToHtml(base, styles)).toContain(' ')
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// SocialBlock
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
describe('renderBlockToHtml — social block', () => {
|
|
396
|
+
const base: SocialBlock = {
|
|
397
|
+
id: 'soc1',
|
|
398
|
+
type: 'social',
|
|
399
|
+
links: [
|
|
400
|
+
{ id: 'sl1', platform: 'linkedin', url: 'https://linkedin.com/in/test' },
|
|
401
|
+
{ id: 'sl2', platform: 'twitter', url: 'https://twitter.com/test' },
|
|
402
|
+
],
|
|
403
|
+
iconSize: 24,
|
|
404
|
+
alignment: 'center',
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
it('includes LinkedIn label text', () => {
|
|
408
|
+
expect(renderBlockToHtml(base, styles)).toContain('LinkedIn')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('includes Twitter label text', () => {
|
|
412
|
+
expect(renderBlockToHtml(base, styles)).toContain('Twitter')
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('includes linkedin URL in anchor href', () => {
|
|
416
|
+
expect(renderBlockToHtml(base, styles)).toContain('https://linkedin.com/in/test')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('applies alignment via td align', () => {
|
|
420
|
+
expect(renderBlockToHtml(base, styles)).toContain('align="center"')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('applies left alignment', () => {
|
|
424
|
+
const html = renderBlockToHtml({ ...base, alignment: 'left' }, styles)
|
|
425
|
+
expect(html).toContain('align="left"')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('applies LinkedIn brand color (#0A66C2)', () => {
|
|
429
|
+
expect(renderBlockToHtml(base, styles)).toContain('#0A66C2')
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('applies Twitter brand color (#1DA1F2)', () => {
|
|
433
|
+
expect(renderBlockToHtml(base, styles)).toContain('#1DA1F2')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('returns empty string when all links have no URL', () => {
|
|
437
|
+
const block: SocialBlock = {
|
|
438
|
+
...base,
|
|
439
|
+
links: [
|
|
440
|
+
{ id: 'sl1', platform: 'linkedin', url: '' },
|
|
441
|
+
{ id: 'sl2', platform: 'twitter', url: '' },
|
|
442
|
+
],
|
|
443
|
+
}
|
|
444
|
+
expect(renderBlockToHtml(block, styles)).toBe('')
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('excludes links with empty URL from output', () => {
|
|
448
|
+
const block: SocialBlock = {
|
|
449
|
+
...base,
|
|
450
|
+
links: [
|
|
451
|
+
{ id: 'sl1', platform: 'linkedin', url: '' },
|
|
452
|
+
{ id: 'sl2', platform: 'twitter', url: 'https://twitter.com/test' },
|
|
453
|
+
],
|
|
454
|
+
}
|
|
455
|
+
const html = renderBlockToHtml(block, styles)
|
|
456
|
+
expect(html).not.toContain('LinkedIn')
|
|
457
|
+
expect(html).toContain('Twitter')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('renders all supported platform labels', () => {
|
|
461
|
+
const block: SocialBlock = {
|
|
462
|
+
...base,
|
|
463
|
+
links: [
|
|
464
|
+
{ id: '1', platform: 'facebook', url: 'https://fb.com' },
|
|
465
|
+
{ id: '2', platform: 'instagram', url: 'https://ig.com' },
|
|
466
|
+
{ id: '3', platform: 'youtube', url: 'https://yt.com' },
|
|
467
|
+
{ id: '4', platform: 'github', url: 'https://gh.com' },
|
|
468
|
+
{ id: '5', platform: 'website', url: 'https://site.com' },
|
|
469
|
+
],
|
|
470
|
+
}
|
|
471
|
+
const html = renderBlockToHtml(block, styles)
|
|
472
|
+
expect(html).toContain('Facebook')
|
|
473
|
+
expect(html).toContain('Instagram')
|
|
474
|
+
expect(html).toContain('YouTube')
|
|
475
|
+
expect(html).toContain('GitHub')
|
|
476
|
+
expect(html).toContain('Website')
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// HeaderBlock
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
describe('renderBlockToHtml — header block', () => {
|
|
485
|
+
const base: HeaderBlock = {
|
|
486
|
+
id: 'hdr1',
|
|
487
|
+
type: 'header',
|
|
488
|
+
companyName: 'Render Corp',
|
|
489
|
+
alignment: 'center',
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
it('includes company name', () => {
|
|
493
|
+
expect(renderBlockToHtml(base, styles)).toContain('Render Corp')
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('applies alignment via td align', () => {
|
|
497
|
+
expect(renderBlockToHtml(base, styles)).toContain('align="center"')
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('applies left alignment', () => {
|
|
501
|
+
const html = renderBlockToHtml({ ...base, alignment: 'left' }, styles)
|
|
502
|
+
expect(html).toContain('align="left"')
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('includes logo img when logoUrl is provided', () => {
|
|
506
|
+
const html = renderBlockToHtml({ ...base, logoUrl: 'https://cdn.example.com/logo.png' }, styles)
|
|
507
|
+
expect(html).toContain('src="https://cdn.example.com/logo.png"')
|
|
508
|
+
expect(html).toContain('alt="Render Corp"')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('does not include img tag when logoUrl is absent', () => {
|
|
512
|
+
expect(renderBlockToHtml(base, styles)).not.toContain('<img')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('wraps in table element', () => {
|
|
516
|
+
expect(renderBlockToHtml(base, styles)).toContain('<table')
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// FooterBlock
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
describe('renderBlockToHtml — footer block', () => {
|
|
525
|
+
const base: FooterBlock = {
|
|
526
|
+
id: 'ftr1',
|
|
527
|
+
type: 'footer',
|
|
528
|
+
companyName: 'Simpli Co',
|
|
529
|
+
address: '100 Startup Lane',
|
|
530
|
+
showUnsubscribe: true,
|
|
531
|
+
unsubscribeUrl: 'https://simpli.co/unsub',
|
|
532
|
+
alignment: 'center',
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
it('includes company name', () => {
|
|
536
|
+
expect(renderBlockToHtml(base, styles)).toContain('Simpli Co')
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('includes address when provided', () => {
|
|
540
|
+
expect(renderBlockToHtml(base, styles)).toContain('100 Startup Lane')
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('omits address when not provided', () => {
|
|
544
|
+
const html = renderBlockToHtml({ ...base, address: undefined }, styles)
|
|
545
|
+
expect(html).not.toContain('100 Startup Lane')
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('includes unsubscribe link text when showUnsubscribe is true', () => {
|
|
549
|
+
expect(renderBlockToHtml(base, styles)).toContain('Unsubscribe')
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('includes unsubscribe URL', () => {
|
|
553
|
+
expect(renderBlockToHtml(base, styles)).toContain('https://simpli.co/unsub')
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('falls back to # for unsubscribe URL when not provided', () => {
|
|
557
|
+
const html = renderBlockToHtml({ ...base, unsubscribeUrl: undefined }, styles)
|
|
558
|
+
expect(html).toContain('href="#"')
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('omits unsubscribe element when showUnsubscribe is false', () => {
|
|
562
|
+
const html = renderBlockToHtml({ ...base, showUnsubscribe: false }, styles)
|
|
563
|
+
expect(html).not.toContain('Unsubscribe')
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
it('applies alignment via td align', () => {
|
|
567
|
+
expect(renderBlockToHtml(base, styles)).toContain('align="center"')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('applies small font-size for footer text', () => {
|
|
571
|
+
expect(renderBlockToHtml(base, styles)).toContain('font-size:12px')
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('wraps in table element', () => {
|
|
575
|
+
expect(renderBlockToHtml(base, styles)).toContain('<table')
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Block style (padding / margin / background) applied to all blocks
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
describe('renderBlockToHtml — block-level style (blockPaddingStyle)', () => {
|
|
584
|
+
it('applies paddingLeft and paddingRight from block.style', () => {
|
|
585
|
+
const block: TextBlock = {
|
|
586
|
+
id: 't1',
|
|
587
|
+
type: 'text',
|
|
588
|
+
content: 'X',
|
|
589
|
+
style: { paddingLeft: 16, paddingRight: 16 },
|
|
590
|
+
}
|
|
591
|
+
const html = renderBlockToHtml(block, styles)
|
|
592
|
+
expect(html).toContain('padding-left:16px')
|
|
593
|
+
expect(html).toContain('padding-right:16px')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('applies marginTop and marginBottom from block.style', () => {
|
|
597
|
+
const block: TextBlock = {
|
|
598
|
+
id: 't1',
|
|
599
|
+
type: 'text',
|
|
600
|
+
content: 'X',
|
|
601
|
+
style: { marginTop: 8, marginBottom: 8 },
|
|
602
|
+
}
|
|
603
|
+
const html = renderBlockToHtml(block, styles)
|
|
604
|
+
expect(html).toContain('margin-top:8px')
|
|
605
|
+
expect(html).toContain('margin-bottom:8px')
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('applies backgroundColor from block.style', () => {
|
|
609
|
+
const block: TextBlock = {
|
|
610
|
+
id: 't1',
|
|
611
|
+
type: 'text',
|
|
612
|
+
content: 'X',
|
|
613
|
+
style: { backgroundColor: '#fefefe' },
|
|
614
|
+
}
|
|
615
|
+
const html = renderBlockToHtml(block, styles)
|
|
616
|
+
expect(html).toContain('background-color:#fefefe')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('returns empty string for an unknown block type via default case', () => {
|
|
620
|
+
// Cast to Block to simulate an unhandled type reaching the switch default
|
|
621
|
+
const unknownBlock = { id: 'x', type: 'unknown' } as unknown as Block
|
|
622
|
+
expect(renderBlockToHtml(unknownBlock, styles)).toBe('')
|
|
623
|
+
})
|
|
624
|
+
})
|