Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tests/smoke/render/render-email.test.ts
12925 views
1
/*
2
* render-email.test.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*
6
*/
7
8
import { existsSync } from "../../../src/deno_ral/fs.ts";
9
import { TestContext, Verify } from "../../test.ts";
10
import { docs } from "../../utils.ts";
11
import { dirname, join } from "../../../src/deno_ral/path.ts";
12
import { testProjectRender } from "../project/common.ts";
13
import { fileExists, validJsonWithFields } from "../../verify.ts";
14
import { testRender } from "./render.ts";
15
import { assert } from "testing/asserts";
16
17
const jsonFile = docs("email/.output_metadata.json");
18
const previewFile = docs("email/email-preview/index.html")
19
const previewFileV2_1 = docs("email/email-preview/email_id-1.html")
20
const previewFileV2_2 = docs("email/email-preview/email_id-2.html")
21
22
23
// Custom verification for multi-email v2 format that checks multiple emails in the array
24
const validJsonWithMultipleEmails = (
25
file: string,
26
expectedEmailCount: number,
27
expectedFieldsPerEmail: Record<string, Record<string, unknown>>
28
): Verify => {
29
return {
30
name: `Valid Json ${file} with ${expectedEmailCount} emails`,
31
verify: (_output) => {
32
const jsonStr = Deno.readTextFileSync(file);
33
const json = JSON.parse(jsonStr);
34
35
// Check rsc_email_version
36
assert(json.rsc_email_version === 2, "rsc_email_version should be 2");
37
assert(Array.isArray(json.emails), "emails should be an array");
38
assert(
39
json.emails.length === expectedEmailCount,
40
`Expected ${expectedEmailCount} emails, got ${json.emails.length}`
41
);
42
43
// Check specific fields in each email
44
for (const [emailIndex, expectedFields] of Object.entries(expectedFieldsPerEmail)) {
45
const idx = parseInt(emailIndex, 10);
46
const email = json.emails[idx];
47
assert(email, `Email at index ${idx} not found`);
48
49
for (const [key, expectedValue] of Object.entries(expectedFields)) {
50
const actualValue = email[key];
51
assert(
52
JSON.stringify(actualValue) === JSON.stringify(expectedValue),
53
`Email #${idx + 1} field ${key} mismatch. Expected: ${JSON.stringify(expectedValue)}, Got: ${JSON.stringify(actualValue)}`
54
);
55
}
56
}
57
58
return Promise.resolve();
59
}
60
};
61
};
62
63
64
const cleanupCtx: TestContext = {
65
teardown: () => {
66
if (existsSync(jsonFile)) {
67
Deno.removeSync(jsonFile);
68
}
69
// Clean up the preview file that exists (v1 format: index.html)
70
if (existsSync(previewFile)) {
71
Deno.removeSync(previewFile);
72
}
73
// Clean up any v2 preview files matching email_id-*.html pattern
74
const previewDir = dirname(previewFile);
75
if (existsSync(previewDir)) {
76
for (const entry of Deno.readDirSync(previewDir)) {
77
if (entry.isFile && /^email_id-\d+\.html$/.test(entry.name)) {
78
Deno.removeSync(join(previewDir, entry.name));
79
}
80
}
81
// Remove the preview directory itself
82
Deno.removeSync(previewDir);
83
}
84
return Promise.resolve();
85
},
86
};
87
88
// Test a basic email render, verifies that the outputs are about what is expected
89
testRender(docs("email/email.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line."})], cleanupCtx);
90
91
// Test basic attachment render, which will validate that attachment shows up in JSON
92
testRender(docs("email/email-attach.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line.", "rsc_email_attachments": ["raw_data.csv"]})], cleanupCtx);
93
94
// Test an email render that has no subject line, this verifies that `rsc_email_subject` key is present and the value is an empty string
95
testRender(docs("email/email-no-subject.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": ""})], cleanupCtx);
96
97
// Test an email render that has a subject line after the email div, this verifies that `rsc_email_subject` key is present
98
testRender(docs("email/email-subject-document-level.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line, after the email div.", "rsc_email_body_text": "An optional text-only version of the email message.\n"})], cleanupCtx);
99
100
// V2 format tests - Connect 2026.03+ with multi-email support
101
// Verify the v2 format includes rsc_email_version and emails array with expected structure
102
testRender(docs("email/email.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {
103
"0": {
104
"email_id": 1,
105
"subject": "The subject line.",
106
"attachments": [],
107
"suppress_scheduled": false,
108
"send_report_as_attachment": false
109
}
110
})], {
111
...cleanupCtx,
112
env: {
113
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
114
}
115
});
116
117
// Test attachment with v2 format
118
testRender(docs("email/email-attach.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {
119
"0": {
120
"email_id": 1,
121
"subject": "The subject line.",
122
"attachments": ["raw_data.csv"],
123
"suppress_scheduled": false,
124
"send_report_as_attachment": false
125
}
126
})], {
127
...cleanupCtx,
128
env: {
129
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
130
}
131
});
132
133
// Test no subject with v2 format
134
testRender(docs("email/email-no-subject.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {
135
"0": {
136
"email_id": 1,
137
"subject": "",
138
"attachments": [],
139
"suppress_scheduled": false,
140
"send_report_as_attachment": false
141
}
142
})], {
143
...cleanupCtx,
144
env: {
145
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
146
}
147
});
148
149
// Test an email render that has a subject line after the email div - this should output v1 format
150
// since the metadata divs are at the document level (outside email), indicating v1 style
151
testRender(docs("email/email-subject-document-level.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line, after the email div.", "rsc_email_body_text": "An optional text-only version of the email message.\n"})], {
152
...cleanupCtx,
153
env: {
154
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
155
}
156
});
157
158
159
// Test multiple emails in v2 format - the core new v2 feature
160
testRender(docs("email/email-multi-v2.qmd"), "email", false, [
161
fileExists(previewFileV2_1),
162
fileExists(previewFileV2_2),
163
validJsonWithMultipleEmails(jsonFile, 2, {
164
"0": {
165
"email_id": 1,
166
"subject": "First Email Subject",
167
"attachments": [],
168
"suppress_scheduled": false,
169
"send_report_as_attachment": false
170
},
171
"1": {
172
"email_id": 2,
173
"subject": "Second Email Subject",
174
"attachments": [],
175
"suppress_scheduled": false,
176
"send_report_as_attachment": false
177
}
178
})
179
], {
180
...cleanupCtx,
181
env: {
182
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
183
}
184
});
185
186
// Test v2 format override with old Connect version
187
// Uses email-format: v2 in YAML to force v2 despite old Connect version
188
testRender(docs("email/email-force-v2.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {
189
"0": {
190
"email_id": 1,
191
"subject": "The subject line.",
192
"attachments": [],
193
"suppress_scheduled": false,
194
"send_report_as_attachment": false
195
}
196
})], {
197
...cleanupCtx,
198
env: {
199
"SPARK_CONNECT_USER_AGENT": "posit-connect/2024.09.0"
200
}
201
});
202
203
// Test mixed metadata - some emails have metadata, others don't
204
testRender(docs("email/email-mixed-metadata-v2.qmd"), "email", false, [
205
fileExists(previewFileV2_1),
206
fileExists(previewFileV2_2),
207
validJsonWithMultipleEmails(jsonFile, 2, {
208
"0": {
209
"email_id": 1,
210
"subject": "First Email Custom Subject",
211
"attachments": [],
212
"suppress_scheduled": false,
213
"send_report_as_attachment": false
214
},
215
"1": {
216
"email_id": 2,
217
"subject": "",
218
"attachments": [],
219
"suppress_scheduled": false,
220
"send_report_as_attachment": false
221
}
222
})
223
], {
224
...cleanupCtx,
225
env: {
226
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
227
}
228
});
229
// Test alternative recipient formats (line-separated and comma-separated plain text)
230
testRender(docs("email/email-recipients-plaintext-formats.qmd"), "email", false, [
231
fileExists(previewFileV2_1),
232
fileExists(previewFileV2_2),
233
fileExists(docs("email/email-preview/email_id-3.html")),
234
fileExists(docs("email/email-preview/email_id-4.html")),
235
fileExists(docs("email/email-preview/email_id-5.html")),
236
fileExists(docs("email/email-preview/email_id-6.html")),
237
validJsonWithMultipleEmails(jsonFile, 6, {
238
"0": {
239
"email_id": 1,
240
"subject": "Line-Separated Recipients",
241
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
242
"attachments": [],
243
"suppress_scheduled": false,
244
"send_report_as_attachment": false
245
},
246
"1": {
247
"email_id": 2,
248
"subject": "Comma-Separated Recipients",
249
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
250
"attachments": [],
251
"suppress_scheduled": false,
252
"send_report_as_attachment": false
253
},
254
"2": {
255
"email_id": 3,
256
"subject": "Funky Email Formats - Dots and Hyphens",
257
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
258
"attachments": [],
259
"suppress_scheduled": false,
260
"send_report_as_attachment": false
261
},
262
"3": {
263
"email_id": 4,
264
"subject": "Funky Email Formats - Plus Signs",
265
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
266
"attachments": [],
267
"suppress_scheduled": false,
268
"send_report_as_attachment": false
269
},
270
"4": {
271
"email_id": 5,
272
"subject": "Mixed Separators and Quotes",
273
"recipients": ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"],
274
"attachments": [],
275
"suppress_scheduled": false,
276
"send_report_as_attachment": false
277
},
278
"5": {
279
"email_id": 6,
280
"subject": "Complex Local Parts",
281
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
282
"attachments": [],
283
"suppress_scheduled": false,
284
"send_report_as_attachment": false
285
}
286
})
287
], {
288
...cleanupCtx,
289
env: {
290
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
291
}
292
});
293
294
// Test all recipient patterns with Python (static inline, conditional inline, metadata, conditional metadata)
295
testRender(docs("email/email-recipients-all-patterns-python.qmd"), "email", false, [
296
fileExists(previewFileV2_1),
297
fileExists(previewFileV2_2),
298
fileExists(docs("email/email-preview/email_id-3.html")),
299
fileExists(docs("email/email-preview/email_id-4.html")),
300
validJsonWithMultipleEmails(jsonFile, 4, {
301
"0": {
302
"email_id": 1,
303
"subject": "Email 1: Static Inline Recipients",
304
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
305
"attachments": [],
306
"suppress_scheduled": false,
307
"send_report_as_attachment": false
308
},
309
"1": {
310
"email_id": 2,
311
"subject": "Email 2: Conditional Inline Recipients",
312
"recipients": ["[email protected]", "[email protected]"],
313
"attachments": [],
314
"suppress_scheduled": false,
315
"send_report_as_attachment": false
316
},
317
"2": {
318
"email_id": 3,
319
"subject": "Email 3: Metadata Attribute Pattern",
320
"recipients": ["[email protected]", "[email protected]"],
321
"attachments": [],
322
"suppress_scheduled": false,
323
"send_report_as_attachment": false
324
},
325
"3": {
326
"email_id": 4,
327
"subject": "Email 4: Conditional Metadata Attribute",
328
"recipients": ["[email protected]", "[email protected]"],
329
"attachments": [],
330
"suppress_scheduled": false,
331
"send_report_as_attachment": false
332
}
333
})
334
], {
335
...cleanupCtx,
336
env: {
337
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
338
}
339
});
340
341
// Test all recipient patterns with R (static inline, conditional inline, metadata, conditional metadata)
342
testRender(docs("email/email-recipients-all-patterns-r.qmd"), "email", false, [
343
fileExists(previewFileV2_1),
344
fileExists(previewFileV2_2),
345
fileExists(docs("email/email-preview/email_id-3.html")),
346
fileExists(docs("email/email-preview/email_id-4.html")),
347
validJsonWithMultipleEmails(jsonFile, 4, {
348
"0": {
349
"email_id": 1,
350
"subject": "Email 1: Static Inline Recipients",
351
"recipients": ["[email protected]", "[email protected]", "[email protected]"],
352
"attachments": [],
353
"suppress_scheduled": false,
354
"send_report_as_attachment": false
355
},
356
"1": {
357
"email_id": 2,
358
"subject": "Email 2: Conditional Inline Recipients",
359
"recipients": ["[email protected]", "[email protected]"],
360
"attachments": [],
361
"suppress_scheduled": false,
362
"send_report_as_attachment": false
363
},
364
"2": {
365
"email_id": 3,
366
"subject": "Email 3: Metadata Attribute Pattern",
367
"recipients": ["[email protected]", "[email protected]"],
368
"attachments": [],
369
"suppress_scheduled": false,
370
"send_report_as_attachment": false
371
},
372
"3": {
373
"email_id": 4,
374
"subject": "Email 4: Conditional Metadata Attribute",
375
"recipients": ["[email protected]", "[email protected]"],
376
"attachments": [],
377
"suppress_scheduled": false,
378
"send_report_as_attachment": false
379
}
380
})
381
], {
382
...cleanupCtx,
383
env: {
384
"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"
385
}
386
});
387
// Render in a project with an output directory set in _quarto.yml and confirm that everything ends up in the output directory
388
testProjectRender(docs("email/project/email-attach.qmd"), "email", (outputDir: string) => {
389
const verify: Verify[]= [];
390
const json = join(outputDir, ".output_metadata.json");
391
const preview = join(outputDir, "email-preview", "index.html");
392
const attachment = join(outputDir, "raw_data.csv");
393
394
verify.push(fileExists(preview));
395
verify.push(fileExists(attachment));
396
verify.push(validJsonWithFields(json, {"rsc_email_subject": "The subject line.", "rsc_email_attachments": ["raw_data.csv"]}));
397
return verify;
398
});
399
400