Path: blob/main/tests/smoke/render/render-email.test.ts
12925 views
/*1* render-email.test.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*5*/67import { existsSync } from "../../../src/deno_ral/fs.ts";8import { TestContext, Verify } from "../../test.ts";9import { docs } from "../../utils.ts";10import { dirname, join } from "../../../src/deno_ral/path.ts";11import { testProjectRender } from "../project/common.ts";12import { fileExists, validJsonWithFields } from "../../verify.ts";13import { testRender } from "./render.ts";14import { assert } from "testing/asserts";1516const jsonFile = docs("email/.output_metadata.json");17const previewFile = docs("email/email-preview/index.html")18const previewFileV2_1 = docs("email/email-preview/email_id-1.html")19const previewFileV2_2 = docs("email/email-preview/email_id-2.html")202122// Custom verification for multi-email v2 format that checks multiple emails in the array23const validJsonWithMultipleEmails = (24file: string,25expectedEmailCount: number,26expectedFieldsPerEmail: Record<string, Record<string, unknown>>27): Verify => {28return {29name: `Valid Json ${file} with ${expectedEmailCount} emails`,30verify: (_output) => {31const jsonStr = Deno.readTextFileSync(file);32const json = JSON.parse(jsonStr);3334// Check rsc_email_version35assert(json.rsc_email_version === 2, "rsc_email_version should be 2");36assert(Array.isArray(json.emails), "emails should be an array");37assert(38json.emails.length === expectedEmailCount,39`Expected ${expectedEmailCount} emails, got ${json.emails.length}`40);4142// Check specific fields in each email43for (const [emailIndex, expectedFields] of Object.entries(expectedFieldsPerEmail)) {44const idx = parseInt(emailIndex, 10);45const email = json.emails[idx];46assert(email, `Email at index ${idx} not found`);4748for (const [key, expectedValue] of Object.entries(expectedFields)) {49const actualValue = email[key];50assert(51JSON.stringify(actualValue) === JSON.stringify(expectedValue),52`Email #${idx + 1} field ${key} mismatch. Expected: ${JSON.stringify(expectedValue)}, Got: ${JSON.stringify(actualValue)}`53);54}55}5657return Promise.resolve();58}59};60};616263const cleanupCtx: TestContext = {64teardown: () => {65if (existsSync(jsonFile)) {66Deno.removeSync(jsonFile);67}68// Clean up the preview file that exists (v1 format: index.html)69if (existsSync(previewFile)) {70Deno.removeSync(previewFile);71}72// Clean up any v2 preview files matching email_id-*.html pattern73const previewDir = dirname(previewFile);74if (existsSync(previewDir)) {75for (const entry of Deno.readDirSync(previewDir)) {76if (entry.isFile && /^email_id-\d+\.html$/.test(entry.name)) {77Deno.removeSync(join(previewDir, entry.name));78}79}80// Remove the preview directory itself81Deno.removeSync(previewDir);82}83return Promise.resolve();84},85};8687// Test a basic email render, verifies that the outputs are about what is expected88testRender(docs("email/email.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line."})], cleanupCtx);8990// Test basic attachment render, which will validate that attachment shows up in JSON91testRender(docs("email/email-attach.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": "The subject line.", "rsc_email_attachments": ["raw_data.csv"]})], cleanupCtx);9293// Test an email render that has no subject line, this verifies that `rsc_email_subject` key is present and the value is an empty string94testRender(docs("email/email-no-subject.qmd"), "email", false, [fileExists(previewFile), validJsonWithFields(jsonFile, {"rsc_email_subject": ""})], cleanupCtx);9596// Test an email render that has a subject line after the email div, this verifies that `rsc_email_subject` key is present97testRender(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);9899// V2 format tests - Connect 2026.03+ with multi-email support100// Verify the v2 format includes rsc_email_version and emails array with expected structure101testRender(docs("email/email.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {102"0": {103"email_id": 1,104"subject": "The subject line.",105"attachments": [],106"suppress_scheduled": false,107"send_report_as_attachment": false108}109})], {110...cleanupCtx,111env: {112"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"113}114});115116// Test attachment with v2 format117testRender(docs("email/email-attach.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {118"0": {119"email_id": 1,120"subject": "The subject line.",121"attachments": ["raw_data.csv"],122"suppress_scheduled": false,123"send_report_as_attachment": false124}125})], {126...cleanupCtx,127env: {128"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"129}130});131132// Test no subject with v2 format133testRender(docs("email/email-no-subject.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {134"0": {135"email_id": 1,136"subject": "",137"attachments": [],138"suppress_scheduled": false,139"send_report_as_attachment": false140}141})], {142...cleanupCtx,143env: {144"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"145}146});147148// Test an email render that has a subject line after the email div - this should output v1 format149// since the metadata divs are at the document level (outside email), indicating v1 style150testRender(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"})], {151...cleanupCtx,152env: {153"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"154}155});156157158// Test multiple emails in v2 format - the core new v2 feature159testRender(docs("email/email-multi-v2.qmd"), "email", false, [160fileExists(previewFileV2_1),161fileExists(previewFileV2_2),162validJsonWithMultipleEmails(jsonFile, 2, {163"0": {164"email_id": 1,165"subject": "First Email Subject",166"attachments": [],167"suppress_scheduled": false,168"send_report_as_attachment": false169},170"1": {171"email_id": 2,172"subject": "Second Email Subject",173"attachments": [],174"suppress_scheduled": false,175"send_report_as_attachment": false176}177})178], {179...cleanupCtx,180env: {181"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"182}183});184185// Test v2 format override with old Connect version186// Uses email-format: v2 in YAML to force v2 despite old Connect version187testRender(docs("email/email-force-v2.qmd"), "email", false, [fileExists(previewFileV2_1), validJsonWithMultipleEmails(jsonFile, 1, {188"0": {189"email_id": 1,190"subject": "The subject line.",191"attachments": [],192"suppress_scheduled": false,193"send_report_as_attachment": false194}195})], {196...cleanupCtx,197env: {198"SPARK_CONNECT_USER_AGENT": "posit-connect/2024.09.0"199}200});201202// Test mixed metadata - some emails have metadata, others don't203testRender(docs("email/email-mixed-metadata-v2.qmd"), "email", false, [204fileExists(previewFileV2_1),205fileExists(previewFileV2_2),206validJsonWithMultipleEmails(jsonFile, 2, {207"0": {208"email_id": 1,209"subject": "First Email Custom Subject",210"attachments": [],211"suppress_scheduled": false,212"send_report_as_attachment": false213},214"1": {215"email_id": 2,216"subject": "",217"attachments": [],218"suppress_scheduled": false,219"send_report_as_attachment": false220}221})222], {223...cleanupCtx,224env: {225"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"226}227});228// Test alternative recipient formats (line-separated and comma-separated plain text)229testRender(docs("email/email-recipients-plaintext-formats.qmd"), "email", false, [230fileExists(previewFileV2_1),231fileExists(previewFileV2_2),232fileExists(docs("email/email-preview/email_id-3.html")),233fileExists(docs("email/email-preview/email_id-4.html")),234fileExists(docs("email/email-preview/email_id-5.html")),235fileExists(docs("email/email-preview/email_id-6.html")),236validJsonWithMultipleEmails(jsonFile, 6, {237"0": {238"email_id": 1,239"subject": "Line-Separated Recipients",240"recipients": ["[email protected]", "[email protected]", "[email protected]"],241"attachments": [],242"suppress_scheduled": false,243"send_report_as_attachment": false244},245"1": {246"email_id": 2,247"subject": "Comma-Separated Recipients",248"recipients": ["[email protected]", "[email protected]", "[email protected]"],249"attachments": [],250"suppress_scheduled": false,251"send_report_as_attachment": false252},253"2": {254"email_id": 3,255"subject": "Funky Email Formats - Dots and Hyphens",256"recipients": ["[email protected]", "[email protected]", "[email protected]"],257"attachments": [],258"suppress_scheduled": false,259"send_report_as_attachment": false260},261"3": {262"email_id": 4,263"subject": "Funky Email Formats - Plus Signs",264"recipients": ["[email protected]", "[email protected]", "[email protected]"],265"attachments": [],266"suppress_scheduled": false,267"send_report_as_attachment": false268},269"4": {270"email_id": 5,271"subject": "Mixed Separators and Quotes",272"recipients": ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"],273"attachments": [],274"suppress_scheduled": false,275"send_report_as_attachment": false276},277"5": {278"email_id": 6,279"subject": "Complex Local Parts",280"recipients": ["[email protected]", "[email protected]", "[email protected]"],281"attachments": [],282"suppress_scheduled": false,283"send_report_as_attachment": false284}285})286], {287...cleanupCtx,288env: {289"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"290}291});292293// Test all recipient patterns with Python (static inline, conditional inline, metadata, conditional metadata)294testRender(docs("email/email-recipients-all-patterns-python.qmd"), "email", false, [295fileExists(previewFileV2_1),296fileExists(previewFileV2_2),297fileExists(docs("email/email-preview/email_id-3.html")),298fileExists(docs("email/email-preview/email_id-4.html")),299validJsonWithMultipleEmails(jsonFile, 4, {300"0": {301"email_id": 1,302"subject": "Email 1: Static Inline Recipients",303"recipients": ["[email protected]", "[email protected]", "[email protected]"],304"attachments": [],305"suppress_scheduled": false,306"send_report_as_attachment": false307},308"1": {309"email_id": 2,310"subject": "Email 2: Conditional Inline Recipients",311"recipients": ["[email protected]", "[email protected]"],312"attachments": [],313"suppress_scheduled": false,314"send_report_as_attachment": false315},316"2": {317"email_id": 3,318"subject": "Email 3: Metadata Attribute Pattern",319"recipients": ["[email protected]", "[email protected]"],320"attachments": [],321"suppress_scheduled": false,322"send_report_as_attachment": false323},324"3": {325"email_id": 4,326"subject": "Email 4: Conditional Metadata Attribute",327"recipients": ["[email protected]", "[email protected]"],328"attachments": [],329"suppress_scheduled": false,330"send_report_as_attachment": false331}332})333], {334...cleanupCtx,335env: {336"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"337}338});339340// Test all recipient patterns with R (static inline, conditional inline, metadata, conditional metadata)341testRender(docs("email/email-recipients-all-patterns-r.qmd"), "email", false, [342fileExists(previewFileV2_1),343fileExists(previewFileV2_2),344fileExists(docs("email/email-preview/email_id-3.html")),345fileExists(docs("email/email-preview/email_id-4.html")),346validJsonWithMultipleEmails(jsonFile, 4, {347"0": {348"email_id": 1,349"subject": "Email 1: Static Inline Recipients",350"recipients": ["[email protected]", "[email protected]", "[email protected]"],351"attachments": [],352"suppress_scheduled": false,353"send_report_as_attachment": false354},355"1": {356"email_id": 2,357"subject": "Email 2: Conditional Inline Recipients",358"recipients": ["[email protected]", "[email protected]"],359"attachments": [],360"suppress_scheduled": false,361"send_report_as_attachment": false362},363"2": {364"email_id": 3,365"subject": "Email 3: Metadata Attribute Pattern",366"recipients": ["[email protected]", "[email protected]"],367"attachments": [],368"suppress_scheduled": false,369"send_report_as_attachment": false370},371"3": {372"email_id": 4,373"subject": "Email 4: Conditional Metadata Attribute",374"recipients": ["[email protected]", "[email protected]"],375"attachments": [],376"suppress_scheduled": false,377"send_report_as_attachment": false378}379})380], {381...cleanupCtx,382env: {383"SPARK_CONNECT_USER_AGENT": "posit-connect/2026.03.0"384}385});386// Render in a project with an output directory set in _quarto.yml and confirm that everything ends up in the output directory387testProjectRender(docs("email/project/email-attach.qmd"), "email", (outputDir: string) => {388const verify: Verify[]= [];389const json = join(outputDir, ".output_metadata.json");390const preview = join(outputDir, "email-preview", "index.html");391const attachment = join(outputDir, "raw_data.csv");392393verify.push(fileExists(preview));394verify.push(fileExists(attachment));395verify.push(validJsonWithFields(json, {"rsc_email_subject": "The subject line.", "rsc_email_attachments": ["raw_data.csv"]}));396return verify;397});398399400