From 646c64431b440230230a1accd13c12a7a43af587 Mon Sep 17 00:00:00 2001 From: JuliBot Date: Tue, 19 May 2026 11:12:50 +0000 Subject: [PATCH 1/2] refactor: separate identity resolution from replaceVariables, enabling unit tests (#80) Extract the three messenger API calls (getComposeDetails, identities.get, accounts.list) into a new exported `resolveIdentityVars(tabId)` helper. Change `replaceVariables` to accept a pre-resolved vars object instead of a tabId, making it a pure synchronous function that only calls `applyVariables`. Update `insertTemplateIntoTab` to call `resolveIdentityVars` once and pass the result to all `replaceVariables` invocations. Add 11 unit tests for the now-infrastructure-free `replaceVariables`. Co-Authored-By: Claude Sonnet 4.6 --- modules/template-insert.js | 57 +++++++++++++++++++------------ tests/template-insert.test.js | 63 ++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/modules/template-insert.js b/modules/template-insert.js index 966684d..40906bd 100644 --- a/modules/template-insert.js +++ b/modules/template-insert.js @@ -122,21 +122,14 @@ export function applyVariables(text, vars, isHtml = false) { } /** - * Replace template variables in text. - * - * Supported tokens: {DATE}, {TIME}, {DATETIME}, {YEAR}, {WEEKDAY}, - * {SENDER_NAME}, {SENDER_EMAIL}, {ACCOUNT_NAME}, {ACCOUNT_EMAIL}. - * - * @param {string} text - Text containing placeholders - * @param {number} tabId - The compose tab ID (used to resolve sender identity) - * @param {boolean} isHtml - Pass true when substituting into HTML to HTML-encode identity values. - * @returns {Promise} Text with placeholders replaced - * @see applyVariables + * Resolve the sender identity variables for a compose tab by calling the + * three Thunderbird messenger APIs that require live tab/account context. + * Separating this from `replaceVariables` keeps the substitution logic pure + * and independently testable. + * @param {number} tabId + * @returns {Promise<{senderName: string, senderEmail: string, accountName: string, accountEmail: string}>} */ -export async function replaceVariables(text, tabId, isHtml = false) { - if (!text) return text; - - const now = new Date(); +export async function resolveIdentityVars(tabId) { let senderName = ""; let senderEmail = ""; let accountName = ""; @@ -168,6 +161,26 @@ export async function replaceVariables(text, tabId, isHtml = false) { console.warn("TemplateWing: could not resolve sender identity", err); } + return { senderName, senderEmail, accountName, accountEmail }; +} + +/** + * Replace template variables in text. + * + * Supported tokens: {DATE}, {TIME}, {DATETIME}, {YEAR}, {WEEKDAY}, + * {SENDER_NAME}, {SENDER_EMAIL}, {ACCOUNT_NAME}, {ACCOUNT_EMAIL}. + * + * @param {string} text - Text containing placeholders + * @param {object} vars - Pre-resolved identity vars: { senderName, senderEmail, accountName, accountEmail }. + * Obtain via {@link resolveIdentityVars} before calling this function. + * @param {boolean} isHtml - Pass true when substituting into HTML to HTML-encode identity values. + * @returns {string} Text with placeholders replaced + * @see applyVariables + * @see resolveIdentityVars + */ +export function replaceVariables(text, vars, isHtml = false) { + if (!text) return text; + const now = new Date(); return applyVariables( text, { @@ -176,10 +189,11 @@ export async function replaceVariables(text, tabId, isHtml = false) { datetime: now.toLocaleDateString() + " " + now.toLocaleTimeString(), year: now.getFullYear(), weekday: WEEKDAY_NAMES[now.getDay()], - senderName, - senderEmail, - accountName, - accountEmail, + senderName: "", + senderEmail: "", + accountName: "", + accountEmail: "", + ...vars, }, isHtml ); @@ -310,9 +324,10 @@ export async function insertTemplateIntoTab(tabId, template) { // "cursor" mode is delivered via a compose script message rather than by // rewriting the whole body, so the signature and any text the user has // already typed stay intact. + const identityVars = await resolveIdentityVars(tabId); let insertedAtCursor = false; if (resolvedBody && mode === INSERT_MODES.CURSOR) { - const body = await replaceVariables(resolvedBody, tabId, true); + const body = replaceVariables(resolvedBody, identityVars, true); const existing = await messenger.compose.getComposeDetails(tabId); const isPlainText = !!existing.isPlainText; console.log("TemplateWing: cursor mode -> sending insertAtCursor", { tabId, isPlainText }); @@ -377,7 +392,7 @@ export async function insertTemplateIntoTab(tabId, template) { ); } } else if (resolvedBody) { - const body = await replaceVariables(resolvedBody, tabId, true); + const body = replaceVariables(resolvedBody, identityVars, true); if (mode === INSERT_MODES.REPLACE) { details.body = body; } else if (mode === INSERT_MODES.PREPEND) { @@ -398,7 +413,7 @@ export async function insertTemplateIntoTab(tabId, template) { } if (template.subject) { - details.subject = await replaceVariables(template.subject, tabId); + details.subject = replaceVariables(template.subject, identityVars); } if (template.to && template.to.length > 0) { diff --git a/tests/template-insert.test.js b/tests/template-insert.test.js index fae480a..f805b95 100644 --- a/tests/template-insert.test.js +++ b/tests/template-insert.test.js @@ -5,7 +5,7 @@ import { installMessengerMock, uninstallMessengerMock } from "./_mock-messenger. // The module installs a storage listener at import time; install the mock first. installMessengerMock(); -const { TEMPLATE_INCLUDE_REGEX, WEEKDAY_NAMES, applyVariables, resolveNestedTemplates } = +const { TEMPLATE_INCLUDE_REGEX, WEEKDAY_NAMES, applyVariables, replaceVariables, resolveNestedTemplates } = await import("../modules/template-insert.js"); after(() => uninstallMessengerMock()); @@ -118,6 +118,67 @@ describe("applyVariables", () => { }); }); +describe("replaceVariables", () => { + it("replaces {SENDER_NAME} and {SENDER_EMAIL} from provided vars", () => { + const result = replaceVariables("From: {SENDER_NAME} <{SENDER_EMAIL}>", { + senderName: "Alice", + senderEmail: "alice@example.com", + }); + assert.strictEqual(result, "From: Alice "); + }); + + it("replaces {ACCOUNT_NAME} and {ACCOUNT_EMAIL} from provided vars", () => { + const result = replaceVariables("{ACCOUNT_NAME} / {ACCOUNT_EMAIL}", { + accountName: "Work", + accountEmail: "alice.work@example.com", + }); + assert.strictEqual(result, "Work / alice.work@example.com"); + }); + + it("HTML-encodes identity values when isHtml is true", () => { + const result = replaceVariables("{SENDER_NAME}", { senderName: "Alice & Bob" }, true); + assert.strictEqual(result, "<b>Alice & Bob</b>"); + }); + + it("does not HTML-encode when isHtml is false", () => { + const result = replaceVariables("{SENDER_NAME}", { senderName: "" }, false); + assert.strictEqual(result, ""); + }); + + it("defaults identity vars to empty strings when vars is empty", () => { + const result = replaceVariables("{SENDER_NAME} <{SENDER_EMAIL}>", {}); + assert.strictEqual(result, " <>"); + }); + + it("replaces {DATE} with a non-empty string", () => { + const result = replaceVariables("{DATE}", {}); + assert.ok(result.length > 0 && !result.includes("{DATE}"), `{DATE} should be replaced, got: ${result}`); + }); + + it("replaces {TIME} with a non-empty string", () => { + const result = replaceVariables("{TIME}", {}); + assert.ok(result.length > 0 && !result.includes("{TIME}"), `{TIME} should be replaced, got: ${result}`); + }); + + it("replaces {YEAR} with a 4-digit year", () => { + const result = replaceVariables("{YEAR}", {}); + assert.match(result, /^\d{4}$/); + }); + + it("replaces {WEEKDAY} with a day name", () => { + const result = replaceVariables("{WEEKDAY}", {}); + assert.ok(WEEKDAY_NAMES.includes(result), `expected a weekday name, got: ${result}`); + }); + + it("returns null unchanged for null input", () => { + assert.strictEqual(replaceVariables(null, {}), null); + }); + + it("returns undefined unchanged for undefined input", () => { + assert.strictEqual(replaceVariables(undefined, {}), undefined); + }); +}); + describe("WEEKDAY_NAMES", () => { it("has seven English weekday names starting with Sunday", () => { assert.strictEqual(WEEKDAY_NAMES.length, 7); From 442080749c52211297062a29b33c113b29682bb1 Mon Sep 17 00:00:00 2001 From: JuliaKalder Date: Wed, 3 Jun 2026 14:13:51 +0200 Subject: [PATCH 2/2] style: apply prettier formatting to tests/template-insert.test.js Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/template-insert.test.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/template-insert.test.js b/tests/template-insert.test.js index f805b95..69df275 100644 --- a/tests/template-insert.test.js +++ b/tests/template-insert.test.js @@ -5,8 +5,13 @@ import { installMessengerMock, uninstallMessengerMock } from "./_mock-messenger. // The module installs a storage listener at import time; install the mock first. installMessengerMock(); -const { TEMPLATE_INCLUDE_REGEX, WEEKDAY_NAMES, applyVariables, replaceVariables, resolveNestedTemplates } = - await import("../modules/template-insert.js"); +const { + TEMPLATE_INCLUDE_REGEX, + WEEKDAY_NAMES, + applyVariables, + replaceVariables, + resolveNestedTemplates, +} = await import("../modules/template-insert.js"); after(() => uninstallMessengerMock()); @@ -152,12 +157,18 @@ describe("replaceVariables", () => { it("replaces {DATE} with a non-empty string", () => { const result = replaceVariables("{DATE}", {}); - assert.ok(result.length > 0 && !result.includes("{DATE}"), `{DATE} should be replaced, got: ${result}`); + assert.ok( + result.length > 0 && !result.includes("{DATE}"), + `{DATE} should be replaced, got: ${result}` + ); }); it("replaces {TIME} with a non-empty string", () => { const result = replaceVariables("{TIME}", {}); - assert.ok(result.length > 0 && !result.includes("{TIME}"), `{TIME} should be replaced, got: ${result}`); + assert.ok( + result.length > 0 && !result.includes("{TIME}"), + `{TIME} should be replaced, got: ${result}` + ); }); it("replaces {YEAR} with a 4-digit year", () => {