unit tets first try
This commit is contained in:
parent
216bdb3022
commit
2c739d0a0c
9 changed files with 1256 additions and 7 deletions
|
|
@ -45,7 +45,7 @@ export function getMiczItUrl(path) {
|
||||||
return `https://micz.it/${prefix}${path}`;
|
return `https://micz.it/${prefix}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixMsgHeader(msgHeader) {
|
export function fixMsgHeader(msgHeader) {
|
||||||
if (!msgHeader.bccList) {
|
if (!msgHeader.bccList) {
|
||||||
msgHeader.bccList = [];
|
msgHeader.bccList = [];
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ export async function getCurrentIdentity(msgHeader, getFull = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function extractEmail(text) {
|
export function extractEmail(text) {
|
||||||
if((text=='')||(text==undefined)) return '';
|
if((text=='')||(text==undefined)) return '';
|
||||||
const emailRegex = /[\w.-]+@[\w.-]+\.\w+/;
|
const emailRegex = /[\w.-]+@[\w.-]+\.\w+/;
|
||||||
const match = text.match(emailRegex);
|
const match = text.match(emailRegex);
|
||||||
|
|
@ -278,7 +278,7 @@ export function convertNewlinesToBr(text) {
|
||||||
return text.replace(/\r\n/g, '\n').replace(/\n/g, '<br>');
|
return text.replace(/\r\n/g, '\n').replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertBrToNewlines(html) {
|
export function convertBrToNewlines(html) {
|
||||||
return html.replace(/<br\s*\/?>/gi, '\n');
|
return html.replace(/<br\s*\/?>/gi, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,7 +374,7 @@ export function i18nConditionalGet(str) {
|
||||||
return str; // Return the original string if the conditions are not met
|
return str; // Return the original string if the conditions are not met
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareThunderbirdVersions(v1, v2) {
|
export function compareThunderbirdVersions(v1, v2) {
|
||||||
const v1parts = v1.split('.').map(Number);
|
const v1parts = v1.split('.').map(Number);
|
||||||
const v2parts = v2.split('.').map(Number);
|
const v2parts = v2.split('.').map(Number);
|
||||||
|
|
||||||
|
|
@ -472,7 +472,7 @@ export async function assignTagsToMessage(messageId, tags) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTagsKeyFromLabel(tag_names, all_tags_list) {
|
export function getTagsKeyFromLabel(tag_names, all_tags_list) {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
tag_names.forEach(name => {
|
tag_names.forEach(name => {
|
||||||
|
|
@ -488,7 +488,7 @@ function getTagsKeyFromLabel(tag_names, all_tags_list) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeString(input) {
|
export function sanitizeString(input) {
|
||||||
// Define the regex to match valid characters
|
// Define the regex to match valid characters
|
||||||
const validChar = /^[^ ()/{%*<>"]+$/;
|
const validChar = /^[^ ()/{%*<>"]+$/;
|
||||||
// Filter out invalid characters from the string
|
// Filter out invalid characters from the string
|
||||||
|
|
@ -799,7 +799,7 @@ export async function getLocalStorageUsedSpace(){
|
||||||
return formatBytes(customprompts_space);
|
return formatBytes(customprompts_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes, decimals = 2) {
|
export function formatBytes(bytes, decimals = 2) {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const step = 1024;
|
const step = 1024;
|
||||||
const suffixes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
const suffixes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
|
||||||
13
package.json
Normal file
13
package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"description": "Dev-only: unit tests for ThunderAI. Not used by the addon.",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.1.1",
|
||||||
|
"jsdom": "^26.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
tests/mzta-logger.test.js
Normal file
59
tests/mzta-logger.test.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { taLogger } from '../js/mzta-logger.js';
|
||||||
|
|
||||||
|
describe('taLogger', () => {
|
||||||
|
let logSpy, errorSpy, warnSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets prefix and debug flag in constructor', () => {
|
||||||
|
const logger = new taLogger('Test', true);
|
||||||
|
expect(logger.prefix).toBe('[ThunderAI Logger | Test] ');
|
||||||
|
expect(logger.do_debug).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('log() outputs when debug is true', () => {
|
||||||
|
const logger = new taLogger('Test', true);
|
||||||
|
logger.log('hello');
|
||||||
|
expect(logSpy).toHaveBeenCalledWith('[ThunderAI Logger | Test] hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('log() is silent when debug is false', () => {
|
||||||
|
const logger = new taLogger('Test', false);
|
||||||
|
logger.log('hello');
|
||||||
|
expect(logSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('log() with do_debug parameter changes debug state', () => {
|
||||||
|
const logger = new taLogger('Test', false);
|
||||||
|
logger.log('hello', true);
|
||||||
|
expect(logSpy).toHaveBeenCalledWith('[ThunderAI Logger | Test] hello');
|
||||||
|
expect(logger.do_debug).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('error() always outputs regardless of debug flag', () => {
|
||||||
|
const logger = new taLogger('Test', false);
|
||||||
|
logger.error('something broke');
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith('[ThunderAI Logger | Test] something broke');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warn() always outputs regardless of debug flag', () => {
|
||||||
|
const logger = new taLogger('Test', false);
|
||||||
|
logger.warn('watch out');
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith('[ThunderAI Logger | Test] watch out');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changeDebug() updates the debug flag', () => {
|
||||||
|
const logger = new taLogger('Test', false);
|
||||||
|
expect(logger.do_debug).toBe(false);
|
||||||
|
logger.changeDebug(true);
|
||||||
|
expect(logger.do_debug).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
206
tests/mzta-placeholders.test.js
Normal file
206
tests/mzta-placeholders.test.js
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import {
|
||||||
|
mapPlaceholderToSuggestion,
|
||||||
|
prepareCustomDataPHsForExport,
|
||||||
|
placeholdersUtils,
|
||||||
|
} from '../js/mzta-placeholders.js';
|
||||||
|
|
||||||
|
// ─── mapPlaceholderToSuggestion ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('mapPlaceholderToSuggestion', () => {
|
||||||
|
it('transforms non-dynamic placeholder', () => {
|
||||||
|
const p = { id: 'mail_subject', type: 'default', is_dynamic: 0 };
|
||||||
|
expect(mapPlaceholderToSuggestion(p)).toEqual({
|
||||||
|
command: '{%mail_subject%}',
|
||||||
|
type: 'default',
|
||||||
|
is_dynamic: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transforms dynamic placeholder (adds colon)', () => {
|
||||||
|
const p = { id: 'mail_headers', type: 'default', is_dynamic: 1 };
|
||||||
|
expect(mapPlaceholderToSuggestion(p)).toEqual({
|
||||||
|
command: '{%mail_headers:%}',
|
||||||
|
type: 'default',
|
||||||
|
is_dynamic: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── prepareCustomDataPHsForExport ───────────────────────────────
|
||||||
|
|
||||||
|
describe('prepareCustomDataPHsForExport', () => {
|
||||||
|
it('removes idnum from non-default placeholders', () => {
|
||||||
|
const placeholders = [
|
||||||
|
{ id: 'thunderai_custom_test', is_default: 0, idnum: 1, text: 'hello' },
|
||||||
|
];
|
||||||
|
const result = prepareCustomDataPHsForExport(placeholders);
|
||||||
|
expect(result[0]).not.toHaveProperty('idnum');
|
||||||
|
expect(result[0].text).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps idnum for default placeholders', () => {
|
||||||
|
const placeholders = [
|
||||||
|
{ id: 'mail_subject', is_default: 1, idnum: 1 },
|
||||||
|
];
|
||||||
|
const result = prepareCustomDataPHsForExport(placeholders);
|
||||||
|
expect(result[0].idnum).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── placeholdersUtils ───────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('placeholdersUtils.validateCustomDataPH_ID', () => {
|
||||||
|
it('prepends prefix if missing', () => {
|
||||||
|
expect(placeholdersUtils.validateCustomDataPH_ID('my_ph')).toBe('thunderai_custom_my_ph');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns unchanged if prefix already present', () => {
|
||||||
|
expect(placeholdersUtils.validateCustomDataPH_ID('thunderai_custom_my_ph')).toBe('thunderai_custom_my_ph');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.stripCustomDataPH_ID_Prefix', () => {
|
||||||
|
it('removes prefix if present', () => {
|
||||||
|
expect(placeholdersUtils.stripCustomDataPH_ID_Prefix('thunderai_custom_my_ph')).toBe('my_ph');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns unchanged if prefix not present', () => {
|
||||||
|
expect(placeholdersUtils.stripCustomDataPH_ID_Prefix('other_ph')).toBe('other_ph');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.hasPlaceholder', () => {
|
||||||
|
it('detects any placeholder when no specific placeholder given', () => {
|
||||||
|
expect(placeholdersUtils.hasPlaceholder('Hello {%mail_subject%}')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no placeholder present', () => {
|
||||||
|
expect(placeholdersUtils.hasPlaceholder('Hello world')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects specific placeholder', () => {
|
||||||
|
expect(placeholdersUtils.hasPlaceholder('Text {%mail_subject%}', 'mail_subject')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for non-matching specific placeholder', () => {
|
||||||
|
expect(placeholdersUtils.hasPlaceholder('Text {%mail_subject%}', 'author')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects dynamic placeholder with value', () => {
|
||||||
|
expect(placeholdersUtils.hasPlaceholder('Header: {%mail_headers:X-Custom%}', 'mail_headers')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.hasCustomPlaceholder', () => {
|
||||||
|
it('detects thunderai_custom_ placeholders', () => {
|
||||||
|
expect(placeholdersUtils.hasCustomPlaceholder('Text {%thunderai_custom_myph%}')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when none present', () => {
|
||||||
|
expect(placeholdersUtils.hasCustomPlaceholder('Text {%mail_subject%}')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks specific custom placeholder', () => {
|
||||||
|
expect(placeholdersUtils.hasCustomPlaceholder('{%thunderai_custom_test%}', 'thunderai_custom_test')).toBe(true);
|
||||||
|
expect(placeholdersUtils.hasCustomPlaceholder('{%thunderai_custom_other%}', 'thunderai_custom_test')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.getPlaceholdersAdditionalTextArray', () => {
|
||||||
|
it('extracts additional_text placeholders', () => {
|
||||||
|
const result = placeholdersUtils.getPlaceholdersAdditionalTextArray('Text {%additional_text%}');
|
||||||
|
expect(result).toEqual([{ placeholder: '{%additional_text%}', info: '' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts additional_text with custom info', () => {
|
||||||
|
const result = placeholdersUtils.getPlaceholdersAdditionalTextArray('Text {%additional_text:Subject line%}');
|
||||||
|
expect(result).toEqual([{ placeholder: '{%additional_text:Subject line%}', info: 'Subject line' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates entries with same info', () => {
|
||||||
|
const text = '{%additional_text:#1%} and {%additional_text:#1%}';
|
||||||
|
const result = placeholdersUtils.getPlaceholdersAdditionalTextArray(text);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns multiple entries with different info', () => {
|
||||||
|
const text = '{%additional_text:#1%} and {%additional_text:#2%}';
|
||||||
|
const result = placeholdersUtils.getPlaceholdersAdditionalTextArray(text);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when none found', () => {
|
||||||
|
expect(placeholdersUtils.getPlaceholdersAdditionalTextArray('no placeholders')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.failSafePlaceholders', () => {
|
||||||
|
it('returns empty string for null', () => {
|
||||||
|
expect(placeholdersUtils.failSafePlaceholders(null)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for undefined', () => {
|
||||||
|
expect(placeholdersUtils.failSafePlaceholders(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through strings unchanged', () => {
|
||||||
|
expect(placeholdersUtils.failSafePlaceholders('hello')).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through numbers unchanged', () => {
|
||||||
|
expect(placeholdersUtils.failSafePlaceholders(42)).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through empty string', () => {
|
||||||
|
expect(placeholdersUtils.failSafePlaceholders('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholdersUtils.replacePlaceholders', () => {
|
||||||
|
it('replaces known placeholders with provided values', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders({
|
||||||
|
text: 'Subject: {%mail_subject%}',
|
||||||
|
replacements: { mail_subject: 'Test Email' },
|
||||||
|
});
|
||||||
|
expect(result).toBe('Subject: Test Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces multiple placeholders', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders({
|
||||||
|
text: 'From: {%author%}, Subject: {%mail_subject%}',
|
||||||
|
replacements: { author: 'John', mail_subject: 'Hello' },
|
||||||
|
});
|
||||||
|
expect(result).toBe('From: John, Subject: Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves unknown placeholders unchanged', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders({
|
||||||
|
text: 'Value: {%nonexistent_thing%}',
|
||||||
|
replacements: {},
|
||||||
|
});
|
||||||
|
expect(result).toBe('Value: {%nonexistent_thing%}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips additional_text when skip_additional_text is true', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders({
|
||||||
|
text: 'Text: {%additional_text%}',
|
||||||
|
replacements: { additional_text: 'replaced' },
|
||||||
|
skip_additional_text: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe('Text: {%additional_text%}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips additional_text with suffix when skip_additional_text is true', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders({
|
||||||
|
text: 'Text: {%additional_text:#1%}',
|
||||||
|
replacements: {},
|
||||||
|
skip_additional_text: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe('Text: {%additional_text:#1%}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty args gracefully', () => {
|
||||||
|
const result = placeholdersUtils.replacePlaceholders();
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
98
tests/mzta-prompts.test.js
Normal file
98
tests/mzta-prompts.test.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { preparePromptsForExport } from '../js/mzta-prompts.js';
|
||||||
|
|
||||||
|
describe('preparePromptsForExport', () => {
|
||||||
|
// Helper: create a custom prompt with integration keys
|
||||||
|
function makeCustomPrompt(overrides = {}) {
|
||||||
|
return {
|
||||||
|
id: 'custom_1',
|
||||||
|
is_default: 0,
|
||||||
|
idnum: 5,
|
||||||
|
text: 'Do something',
|
||||||
|
enabled: 1,
|
||||||
|
api_type: '',
|
||||||
|
chatgpt_api_key: 'key1',
|
||||||
|
chatgpt_model: 'gpt-5',
|
||||||
|
ollama_host: 'http://localhost',
|
||||||
|
ollama_model: 'llama3',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create a default prompt
|
||||||
|
function makeDefaultPrompt(overrides = {}) {
|
||||||
|
return {
|
||||||
|
id: 'prompt_reply',
|
||||||
|
is_default: 1,
|
||||||
|
idnum: 1,
|
||||||
|
text: 'Reply to this email',
|
||||||
|
enabled: 1,
|
||||||
|
position_compose: 1,
|
||||||
|
position_display: 2,
|
||||||
|
need_custom_text: 0,
|
||||||
|
api_type: '',
|
||||||
|
chatgpt_api_key: 'key1',
|
||||||
|
chatgpt_model: 'gpt-5',
|
||||||
|
ollama_host: 'http://localhost',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('removes api_type and integration keys when include_api_settings is false', () => {
|
||||||
|
const prompts = [makeCustomPrompt()];
|
||||||
|
const result = preparePromptsForExport(prompts, false);
|
||||||
|
expect(result[0]).not.toHaveProperty('api_type');
|
||||||
|
expect(result[0]).not.toHaveProperty('chatgpt_api_key');
|
||||||
|
expect(result[0]).not.toHaveProperty('chatgpt_model');
|
||||||
|
expect(result[0]).not.toHaveProperty('ollama_host');
|
||||||
|
expect(result[0]).not.toHaveProperty('ollama_model');
|
||||||
|
expect(result[0].text).toBe('Do something');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes idnum from custom prompts', () => {
|
||||||
|
const prompts = [makeCustomPrompt()];
|
||||||
|
const result = preparePromptsForExport(prompts, false);
|
||||||
|
expect(result[0]).not.toHaveProperty('idnum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for default prompts, keeps only allowed keys when include_api_settings is false', () => {
|
||||||
|
const prompts = [makeDefaultPrompt()];
|
||||||
|
const result = preparePromptsForExport(prompts, false);
|
||||||
|
const keys = Object.keys(result[0]);
|
||||||
|
expect(keys).toContain('id');
|
||||||
|
expect(keys).toContain('enabled');
|
||||||
|
expect(keys).toContain('position_compose');
|
||||||
|
expect(keys).toContain('position_display');
|
||||||
|
expect(keys).toContain('need_custom_text');
|
||||||
|
expect(keys).not.toContain('text');
|
||||||
|
expect(keys).not.toContain('api_type');
|
||||||
|
expect(keys).not.toContain('chatgpt_api_key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps api_type when include_api_settings is true', () => {
|
||||||
|
const prompts = [makeCustomPrompt({ api_type: 'chatgpt_api' })];
|
||||||
|
const result = preparePromptsForExport(prompts, true);
|
||||||
|
expect(result[0].api_type).toBe('chatgpt_api');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps only active integration keys when api_type is set and include_api_settings is true', () => {
|
||||||
|
const prompts = [makeCustomPrompt({ api_type: 'chatgpt_api' })];
|
||||||
|
const result = preparePromptsForExport(prompts, true);
|
||||||
|
// chatgpt keys should remain (active integration = "chatgpt" from "chatgpt_api")
|
||||||
|
expect(result[0]).toHaveProperty('chatgpt_api_key');
|
||||||
|
expect(result[0]).toHaveProperty('chatgpt_model');
|
||||||
|
// ollama keys should be removed
|
||||||
|
expect(result[0]).not.toHaveProperty('ollama_host');
|
||||||
|
expect(result[0]).not.toHaveProperty('ollama_model');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original array', () => {
|
||||||
|
const original = [makeCustomPrompt()];
|
||||||
|
const originalCopy = JSON.parse(JSON.stringify(original));
|
||||||
|
preparePromptsForExport(original, false);
|
||||||
|
expect(original).toEqual(originalCopy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty prompts array', () => {
|
||||||
|
expect(preparePromptsForExport([], false)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
73
tests/mzta-utils-prompt.test.js
Normal file
73
tests/mzta-utils-prompt.test.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { taPromptUtils } from '../js/mzta-utils-prompt.js';
|
||||||
|
|
||||||
|
// ─── finalizePrompt_get_calendar_event ───────────────────────────
|
||||||
|
|
||||||
|
describe('taPromptUtils.finalizePrompt_get_calendar_event', () => {
|
||||||
|
it('removes {%cc_list%} placeholder', () => {
|
||||||
|
const result = taPromptUtils.finalizePrompt_get_calendar_event('Send to {%cc_list%} please');
|
||||||
|
expect(result).toBe('Send to please');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes {%recipients%} placeholder', () => {
|
||||||
|
const result = taPromptUtils.finalizePrompt_get_calendar_event('Invite {%recipients%} now');
|
||||||
|
expect(result).toBe('Invite now');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes both placeholders', () => {
|
||||||
|
const result = taPromptUtils.finalizePrompt_get_calendar_event('To: {%recipients%}, CC: {%cc_list%}');
|
||||||
|
expect(result).toBe('To: , CC: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns prompt unchanged if no placeholders present', () => {
|
||||||
|
const prompt = 'Create a calendar event from this email';
|
||||||
|
expect(taPromptUtils.finalizePrompt_get_calendar_event(prompt)).toBe(prompt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getTagsFromResponse ─────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('taPromptUtils.getTagsFromResponse', () => {
|
||||||
|
it('parses JSON response with tags array', () => {
|
||||||
|
const response = '{"tags": ["urgent", "work"]}';
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(response)).toEqual(['urgent', 'work']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses JSON response with tags as string', () => {
|
||||||
|
const response = '{"tags": "urgent, work"}';
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(response)).toEqual(['urgent', 'work']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles JSON embedded in text', () => {
|
||||||
|
const response = 'Here are the tags: {"tags": ["a", "b"]} done';
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(response)).toEqual(['a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to comma splitting on invalid JSON', () => {
|
||||||
|
const response = 'urgent, work, personal';
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(response)).toEqual(['urgent', 'work', 'personal']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array for empty input', () => {
|
||||||
|
expect(taPromptUtils.getTagsFromResponse('')).toEqual([]);
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(null)).toEqual([]);
|
||||||
|
expect(taPromptUtils.getTagsFromResponse(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters tags when filter_tags is true', () => {
|
||||||
|
const response = '{"tags": ["urgent", "work", "personal"]}';
|
||||||
|
const result = taPromptUtils.getTagsFromResponse(response, true, 'urgent, personal');
|
||||||
|
expect(result).toEqual(['urgent', 'personal']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filter is case-insensitive', () => {
|
||||||
|
const response = '{"tags": ["Urgent", "WORK"]}';
|
||||||
|
const result = taPromptUtils.getTagsFromResponse(response, true, 'urgent, work');
|
||||||
|
expect(result).toEqual(['Urgent', 'WORK']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not filter when filter_tags is false', () => {
|
||||||
|
const response = '{"tags": ["urgent", "work"]}';
|
||||||
|
const result = taPromptUtils.getTagsFromResponse(response, false, 'urgent');
|
||||||
|
expect(result).toEqual(['urgent', 'work']);
|
||||||
|
});
|
||||||
|
});
|
||||||
697
tests/mzta-utils.test.js
Normal file
697
tests/mzta-utils.test.js
Normal file
|
|
@ -0,0 +1,697 @@
|
||||||
|
import {
|
||||||
|
getLanguageDisplayName,
|
||||||
|
sanitizeHtml,
|
||||||
|
sanitizeMailHeaders,
|
||||||
|
stripHtmlKeepLines,
|
||||||
|
htmlBodyToPlainText,
|
||||||
|
cleanupNewlines,
|
||||||
|
convertNewlinesToBr,
|
||||||
|
convertNewlinesToParagraphs,
|
||||||
|
getGPTWebModelString,
|
||||||
|
getMenuContextCompose,
|
||||||
|
getMenuContextDisplay,
|
||||||
|
checkIfTagLabelExists,
|
||||||
|
normalizeStringList,
|
||||||
|
prepareOriginURL,
|
||||||
|
checkAPIIntegration,
|
||||||
|
hasSpecificIntegration,
|
||||||
|
extractJsonObject,
|
||||||
|
isAPIKeyValue,
|
||||||
|
validateChatGPTWebCustomData,
|
||||||
|
sanitizeChatGPTModelData,
|
||||||
|
sanitizeChatGPTWebCustomData,
|
||||||
|
getActiveSpecialPromptsIDs,
|
||||||
|
getConnectionType,
|
||||||
|
getMailBody,
|
||||||
|
generateCallID,
|
||||||
|
ChatGPTWeb_models,
|
||||||
|
fixMsgHeader,
|
||||||
|
extractEmail,
|
||||||
|
convertBrToNewlines,
|
||||||
|
compareThunderbirdVersions,
|
||||||
|
getTagsKeyFromLabel,
|
||||||
|
sanitizeString,
|
||||||
|
formatBytes,
|
||||||
|
} from '../js/mzta-utils.js';
|
||||||
|
|
||||||
|
// ─── Constants ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('ChatGPTWeb_models', () => {
|
||||||
|
it('is an array of model strings', () => {
|
||||||
|
expect(Array.isArray(ChatGPTWeb_models)).toBe(true);
|
||||||
|
expect(ChatGPTWeb_models.length).toBeGreaterThan(0);
|
||||||
|
ChatGPTWeb_models.forEach(m => expect(typeof m).toBe('string'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMenuContextCompose', () => {
|
||||||
|
it('returns compose_action_menu', () => {
|
||||||
|
expect(getMenuContextCompose()).toBe('compose_action_menu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMenuContextDisplay', () => {
|
||||||
|
it('returns message_display_action_menu', () => {
|
||||||
|
expect(getMenuContextDisplay()).toBe('message_display_action_menu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── String / HTML manipulation ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('sanitizeHtml', () => {
|
||||||
|
it('strips HTML tags but keeps <br>', () => {
|
||||||
|
expect(sanitizeHtml('<p>Hello</p><br>World')).toBe('Hello<br>World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps <br/> and <br /> variants', () => {
|
||||||
|
expect(sanitizeHtml('A<br/>B<br />C')).toBe('A<br/>B<br />C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through plain text unchanged', () => {
|
||||||
|
expect(sanitizeHtml('just text')).toBe('just text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
expect(sanitizeHtml('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips nested tags', () => {
|
||||||
|
expect(sanitizeHtml('<div><span>Hi</span></div>')).toBe('Hi');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeMailHeaders', () => {
|
||||||
|
it('escapes < and > to HTML entities', () => {
|
||||||
|
expect(sanitizeMailHeaders('user@example.com <User Name>')).toBe('user@example.com <User Name>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for null/undefined/empty', () => {
|
||||||
|
expect(sanitizeMailHeaders(null)).toBe('');
|
||||||
|
expect(sanitizeMailHeaders(undefined)).toBe('');
|
||||||
|
expect(sanitizeMailHeaders('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles strings without angle brackets', () => {
|
||||||
|
expect(sanitizeMailHeaders('plain text')).toBe('plain text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stripHtmlKeepLines', () => {
|
||||||
|
it('converts </p> to newlines and removes <p>', () => {
|
||||||
|
expect(stripHtmlKeepLines('<p>line1</p><p>line2</p>')).toBe('line1\nline2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts <br> to newlines', () => {
|
||||||
|
expect(stripHtmlKeepLines('line1<br>line2')).toBe('line1\nline2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes other HTML tags', () => {
|
||||||
|
expect(stripHtmlKeepLines('<b>bold</b> <i>italic</i>')).toBe('bold italic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims result', () => {
|
||||||
|
expect(stripHtmlKeepLines(' <p>text</p> ')).toBe('text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('htmlBodyToPlainText', () => {
|
||||||
|
it('extracts text from simple HTML', () => {
|
||||||
|
expect(htmlBodyToPlainText('<p>Hello World</p>')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes style elements', () => {
|
||||||
|
expect(htmlBodyToPlainText('<style>body{color:red}</style><p>text</p>')).toBe('text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes display:none elements', () => {
|
||||||
|
expect(htmlBodyToPlainText('<div style="display:none">hidden</div><p>visible</p>')).toBe('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
expect(htmlBodyToPlainText('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanupNewlines', () => {
|
||||||
|
it('normalizes \\r\\n to \\n', () => {
|
||||||
|
expect(cleanupNewlines('a\r\nb')).toBe('a\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses multiple newlines to one', () => {
|
||||||
|
expect(cleanupNewlines('a\n\n\nb')).toBe('a\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses spaces/tabs before newlines', () => {
|
||||||
|
expect(cleanupNewlines('a \nb')).toBe('a\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces with space', () => {
|
||||||
|
expect(cleanupNewlines('hello world')).toBe('hello world');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims result', () => {
|
||||||
|
expect(cleanupNewlines(' text ')).toBe('text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertNewlinesToBr', () => {
|
||||||
|
it('converts \\n to <br>', () => {
|
||||||
|
expect(convertNewlinesToBr('a\nb')).toBe('a<br>b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles \\r\\n', () => {
|
||||||
|
expect(convertNewlinesToBr('a\r\nb')).toBe('a<br>b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple newlines', () => {
|
||||||
|
expect(convertNewlinesToBr('a\n\nb')).toBe('a<br><br>b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertNewlinesToParagraphs', () => {
|
||||||
|
it('wraps each line in <p> tags', () => {
|
||||||
|
expect(convertNewlinesToParagraphs('line1\nline2')).toBe('<p>line1</p><p>line2</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles single line', () => {
|
||||||
|
expect(convertNewlinesToParagraphs('single')).toBe('<p>single</p>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertBrToNewlines', () => {
|
||||||
|
it('converts <br> to newlines', () => {
|
||||||
|
expect(convertBrToNewlines('a<br>b')).toBe('a\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles <br/> and <br /> variants', () => {
|
||||||
|
expect(convertBrToNewlines('a<br/>b<br />c')).toBe('a\nb\nc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case-insensitive', () => {
|
||||||
|
expect(convertBrToNewlines('a<BR>b<Br>c')).toBe('a\nb\nc');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Model / URL helpers ─────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getGPTWebModelString', () => {
|
||||||
|
it('maps gpt-5 to "5"', () => {
|
||||||
|
expect(getGPTWebModelString('gpt-5')).toBe('5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gpt-5-instant to "5 Fast"', () => {
|
||||||
|
expect(getGPTWebModelString('gpt-5-instant')).toBe('5 Fast');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gpt-5-t-mini to "5 Thinking mini"', () => {
|
||||||
|
expect(getGPTWebModelString('gpt-5-t-mini')).toBe('5 Thinking mini');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gpt-5-thinking to "5 Thinking"', () => {
|
||||||
|
expect(getGPTWebModelString('gpt-5-thinking')).toBe('5 Thinking');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns model string for unknown models', () => {
|
||||||
|
expect(getGPTWebModelString('custom-model')).toBe('custom-model');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for falsy input', () => {
|
||||||
|
expect(getGPTWebModelString(null)).toBe('');
|
||||||
|
expect(getGPTWebModelString('')).toBe('');
|
||||||
|
expect(getGPTWebModelString(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case-insensitive and trims whitespace', () => {
|
||||||
|
expect(getGPTWebModelString(' GPT-5 ')).toBe('5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prepareOriginURL', () => {
|
||||||
|
it('appends /* to URL without trailing slash', () => {
|
||||||
|
expect(prepareOriginURL('https://example.com')).toBe('https://example.com/*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends * to URL with trailing slash', () => {
|
||||||
|
expect(prepareOriginURL('https://example.com/')).toBe('https://example.com/*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Tag helpers ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('checkIfTagLabelExists', () => {
|
||||||
|
const tags = {
|
||||||
|
tag1: { tag: 'Important' },
|
||||||
|
tag2: { tag: 'Work' },
|
||||||
|
tag3: { tag: 'Personal' },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('finds tag case-insensitively', () => {
|
||||||
|
expect(checkIfTagLabelExists('important', tags)).toBe(true);
|
||||||
|
expect(checkIfTagLabelExists('IMPORTANT', tags)).toBe(true);
|
||||||
|
expect(checkIfTagLabelExists('Important', tags)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when tag not found', () => {
|
||||||
|
expect(checkIfTagLabelExists('missing', tags)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty tags list', () => {
|
||||||
|
expect(checkIfTagLabelExists('any', {})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTagsKeyFromLabel', () => {
|
||||||
|
const allTags = {
|
||||||
|
key1: { tag: 'Important' },
|
||||||
|
key2: { tag: 'Work' },
|
||||||
|
key3: { tag: 'Personal' },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('returns keys for matching labels', () => {
|
||||||
|
expect(getTagsKeyFromLabel(['important', 'work'], allTags)).toEqual(['key1', 'key2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array for no matches', () => {
|
||||||
|
expect(getTagsKeyFromLabel(['missing'], allTags)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty input', () => {
|
||||||
|
expect(getTagsKeyFromLabel([], allTags)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── List / string utilities ─────────────────────────────────────
|
||||||
|
|
||||||
|
describe('normalizeStringList', () => {
|
||||||
|
it('deduplicates and sorts comma-separated input', () => {
|
||||||
|
expect(normalizeStringList('b, a, c, a')).toBe('a, b, c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates and sorts newline-separated input', () => {
|
||||||
|
expect(normalizeStringList('b\na\nc\na')).toBe('a, b, c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns comma-separated string by default (returnType=0)', () => {
|
||||||
|
expect(normalizeStringList('b, a', 0)).toBe('a, b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns newline-separated string (returnType=1)', () => {
|
||||||
|
expect(normalizeStringList('b, a', 1)).toBe('a\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns array (returnType=2)', () => {
|
||||||
|
expect(normalizeStringList('b, a', 2)).toEqual(['a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lowercases all items', () => {
|
||||||
|
expect(normalizeStringList('Apple, BANANA', 2)).toEqual(['apple', 'banana']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateCallID', () => {
|
||||||
|
it('returns a string of the specified length', () => {
|
||||||
|
expect(generateCallID(10)).toHaveLength(10);
|
||||||
|
expect(generateCallID(20)).toHaveLength(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to length 10', () => {
|
||||||
|
expect(generateCallID()).toHaveLength(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains only alphanumeric characters', () => {
|
||||||
|
const id = generateCallID(100);
|
||||||
|
expect(id).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Boolean / logic helpers ─────────────────────────────────────
|
||||||
|
|
||||||
|
describe('checkAPIIntegration', () => {
|
||||||
|
it('returns true for non-chatgpt_web connection types', () => {
|
||||||
|
expect(checkAPIIntegration('openai_api', false, null)).toBe(true);
|
||||||
|
expect(checkAPIIntegration('ollama', false, '')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for chatgpt_web with specific integration', () => {
|
||||||
|
expect(checkAPIIntegration('chatgpt_web', true, 'openai_api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for chatgpt_web without specific integration', () => {
|
||||||
|
expect(checkAPIIntegration('chatgpt_web', false, null)).toBe(false);
|
||||||
|
expect(checkAPIIntegration('chatgpt_web', true, null)).toBe(false);
|
||||||
|
expect(checkAPIIntegration('chatgpt_web', true, '')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSpecificIntegration', () => {
|
||||||
|
it('returns true when use is true and conntype is non-empty', () => {
|
||||||
|
expect(hasSpecificIntegration(true, 'openai_api')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when use is false', () => {
|
||||||
|
expect(hasSpecificIntegration(false, 'openai_api')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when conntype is null or empty', () => {
|
||||||
|
expect(hasSpecificIntegration(true, null)).toBe(false);
|
||||||
|
expect(hasSpecificIntegration(true, '')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAPIKeyValue', () => {
|
||||||
|
it('returns true for strings ending with _api_key', () => {
|
||||||
|
expect(isAPIKeyValue('openai_api_key')).toBe(true);
|
||||||
|
expect(isAPIKeyValue('chatgpt_api_key')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for other strings', () => {
|
||||||
|
expect(isAPIKeyValue('openai_model')).toBe(false);
|
||||||
|
expect(isAPIKeyValue('api_key_value')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── JSON extraction ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('extractJsonObject', () => {
|
||||||
|
it('extracts JSON from string with surrounding text', () => {
|
||||||
|
const result = extractJsonObject('Here is the result: {"tags": ["a", "b"]} done');
|
||||||
|
expect(result).toEqual({ tags: ['a', 'b'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns parsed object for valid JSON string', () => {
|
||||||
|
expect(extractJsonObject('{"key": "value"}')).toEqual({ key: 'value' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts nested JSON', () => {
|
||||||
|
const result = extractJsonObject('text {"a": {"b": 1}} more');
|
||||||
|
expect(result).toEqual({ a: { b: 1 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on no JSON found', () => {
|
||||||
|
expect(() => extractJsonObject('no json here')).toThrow('No JSON object found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on invalid JSON', () => {
|
||||||
|
expect(() => extractJsonObject('{invalid json}')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── ChatGPT validation / sanitization ───────────────────────────
|
||||||
|
|
||||||
|
describe('validateChatGPTWebCustomData', () => {
|
||||||
|
it('validates /g/ paths with alphanumeric chars', () => {
|
||||||
|
expect(validateChatGPTWebCustomData('/g/abc-123')).toBe(true);
|
||||||
|
expect(validateChatGPTWebCustomData('/g/my-gpt/test')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid paths', () => {
|
||||||
|
expect(validateChatGPTWebCustomData('invalid')).toBe(false);
|
||||||
|
expect(validateChatGPTWebCustomData('/g/')).toBe(false);
|
||||||
|
expect(validateChatGPTWebCustomData('/g/test space')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts empty string', () => {
|
||||||
|
expect(validateChatGPTWebCustomData('')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeChatGPTModelData', () => {
|
||||||
|
it('encodes and lowercases input', () => {
|
||||||
|
expect(sanitizeChatGPTModelData('GPT-5')).toBe('gpt-5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes special characters', () => {
|
||||||
|
expect(sanitizeChatGPTModelData('model name')).toBe('model%20name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for falsy input', () => {
|
||||||
|
expect(sanitizeChatGPTModelData(null)).toBe('');
|
||||||
|
expect(sanitizeChatGPTModelData('')).toBe('');
|
||||||
|
expect(sanitizeChatGPTModelData(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeChatGPTWebCustomData', () => {
|
||||||
|
it('removes non-alphanumeric/dash/slash characters', () => {
|
||||||
|
expect(sanitizeChatGPTWebCustomData('/g/test-path')).toBe('/g/test-path');
|
||||||
|
expect(sanitizeChatGPTWebCustomData('/g/test path!')).toBe('/g/testpath');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for falsy input', () => {
|
||||||
|
expect(sanitizeChatGPTWebCustomData(null)).toBe('');
|
||||||
|
expect(sanitizeChatGPTWebCustomData('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Special prompts ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getActiveSpecialPromptsIDs', () => {
|
||||||
|
it('returns empty array with all false', () => {
|
||||||
|
expect(getActiveSpecialPromptsIDs({})).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes prompt_add_tags when addtags is true', () => {
|
||||||
|
const result = getActiveSpecialPromptsIDs({ addtags: true });
|
||||||
|
expect(result).toContain('prompt_add_tags');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes calendar and task prompts when enabled', () => {
|
||||||
|
const result = getActiveSpecialPromptsIDs({
|
||||||
|
get_calendar_event: true,
|
||||||
|
get_calendar_event_from_clipboard: true,
|
||||||
|
get_task: true,
|
||||||
|
});
|
||||||
|
expect(result).toContain('prompt_get_calendar_event');
|
||||||
|
expect(result).toContain('prompt_get_calendar_event_from_clipboard');
|
||||||
|
expect(result).toContain('prompt_get_task');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on chatgpt_web: only includes add_tags if both addtags and addtags_api', () => {
|
||||||
|
const result = getActiveSpecialPromptsIDs({
|
||||||
|
is_chatgpt_web: true,
|
||||||
|
addtags: true,
|
||||||
|
addtags_api: true,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(['prompt_add_tags']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on chatgpt_web: excludes add_tags if addtags_api is false', () => {
|
||||||
|
const result = getActiveSpecialPromptsIDs({
|
||||||
|
is_chatgpt_web: true,
|
||||||
|
addtags: true,
|
||||||
|
addtags_api: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on chatgpt_web: excludes calendar/task prompts', () => {
|
||||||
|
const result = getActiveSpecialPromptsIDs({
|
||||||
|
is_chatgpt_web: true,
|
||||||
|
get_calendar_event: true,
|
||||||
|
get_task: true,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getConnectionType ───────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getConnectionType', () => {
|
||||||
|
it('returns default connection_type when no prefix', () => {
|
||||||
|
const prefs = { connection_type: 'openai_api' };
|
||||||
|
expect(getConnectionType(prefs, null)).toBe('openai_api');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns prompt api_type when no specific type and prompt has api_type', () => {
|
||||||
|
const prefs = { connection_type: 'openai_api' };
|
||||||
|
const prompt = { api_type: 'ollama_api' };
|
||||||
|
expect(getConnectionType(prefs, prompt)).toBe('ollama_api');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default when prefs is null', () => {
|
||||||
|
expect(getConnectionType(null, null)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns prompt api_type over default when prompt has api_type', () => {
|
||||||
|
const prefs = { connection_type: 'openai_api' };
|
||||||
|
const prompt = { api_type: 'claude_api' };
|
||||||
|
expect(getConnectionType(prefs, prompt)).toBe('claude_api');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getMailBody ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getMailBody', () => {
|
||||||
|
it('extracts text and html from message parts', () => {
|
||||||
|
const msg = {
|
||||||
|
parts: [
|
||||||
|
{ contentType: 'text/plain', body: 'Hello plain' },
|
||||||
|
{ contentType: 'text/html', body: '<p>Hello html</p>' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(getMailBody(msg)).toEqual({ text: 'Hello plain', html: '<p>Hello html</p>' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts text to html with <br> if no html part', () => {
|
||||||
|
const msg = {
|
||||||
|
parts: [{ contentType: 'text/plain', body: 'line1\nline2' }],
|
||||||
|
};
|
||||||
|
expect(getMailBody(msg)).toEqual({ text: 'line1\nline2', html: 'line1<br>line2' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles nested parts', () => {
|
||||||
|
const msg = {
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
contentType: 'multipart/alternative',
|
||||||
|
parts: [
|
||||||
|
{ contentType: 'text/plain', body: 'nested text' },
|
||||||
|
{ contentType: 'text/html', body: '<p>nested html</p>' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(getMailBody(msg)).toEqual({ text: 'nested text', html: '<p>nested html</p>' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty strings when no parts', () => {
|
||||||
|
expect(getMailBody({ parts: [] })).toEqual({ text: '', html: '' });
|
||||||
|
expect(getMailBody({})).toEqual({ text: '', html: '' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getLanguageDisplayName ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getLanguageDisplayName', () => {
|
||||||
|
it('returns capitalized language name', () => {
|
||||||
|
const name = getLanguageDisplayName('en');
|
||||||
|
expect(name).toBe('English');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns localized name for other languages', () => {
|
||||||
|
const name = getLanguageDisplayName('fr');
|
||||||
|
expect(name.charAt(0)).toBe(name.charAt(0).toUpperCase());
|
||||||
|
expect(name.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── fixMsgHeader ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('fixMsgHeader', () => {
|
||||||
|
it('adds missing bccList, ccList, and recipients', () => {
|
||||||
|
const header = {};
|
||||||
|
const result = fixMsgHeader(header);
|
||||||
|
expect(result.bccList).toEqual([]);
|
||||||
|
expect(result.ccList).toEqual([]);
|
||||||
|
expect(result.recipients).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing values', () => {
|
||||||
|
const header = {
|
||||||
|
bccList: ['a@b.com'],
|
||||||
|
ccList: ['c@d.com'],
|
||||||
|
recipients: ['e@f.com'],
|
||||||
|
};
|
||||||
|
const result = fixMsgHeader(header);
|
||||||
|
expect(result.bccList).toEqual(['a@b.com']);
|
||||||
|
expect(result.ccList).toEqual(['c@d.com']);
|
||||||
|
expect(result.recipients).toEqual(['e@f.com']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── extractEmail ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('extractEmail', () => {
|
||||||
|
it('extracts email from text', () => {
|
||||||
|
expect(extractEmail('John Doe <john@example.com>')).toBe('john@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts first email when multiple present', () => {
|
||||||
|
expect(extractEmail('a@b.com and c@d.com')).toBe('a@b.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for empty/undefined input', () => {
|
||||||
|
expect(extractEmail('')).toBe('');
|
||||||
|
expect(extractEmail(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when no email found', () => {
|
||||||
|
expect(extractEmail('no email here')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── compareThunderbirdVersions ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('compareThunderbirdVersions', () => {
|
||||||
|
it('returns 0 for equal versions', () => {
|
||||||
|
expect(compareThunderbirdVersions('128.0', '128.0')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 1 when first version is greater', () => {
|
||||||
|
expect(compareThunderbirdVersions('129.0', '128.0')).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns -1 when first version is lesser', () => {
|
||||||
|
expect(compareThunderbirdVersions('127.0', '128.0')).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles different length version strings', () => {
|
||||||
|
expect(compareThunderbirdVersions('128.0.1', '128.0')).toBe(1);
|
||||||
|
expect(compareThunderbirdVersions('128', '128.0.0')).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('compares multi-segment versions correctly', () => {
|
||||||
|
expect(compareThunderbirdVersions('128.1.2', '128.1.3')).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── sanitizeString ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('sanitizeString', () => {
|
||||||
|
it('lowercases and removes invalid characters', () => {
|
||||||
|
expect(sanitizeString('Hello World')).toBe('helloworld');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes special characters like < > * "', () => {
|
||||||
|
expect(sanitizeString('test<>*"val')).toBe('testval');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes non-ASCII characters', () => {
|
||||||
|
const result = sanitizeString('café');
|
||||||
|
expect(result).toBe('cafu00e9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates to 29 characters', () => {
|
||||||
|
const long = 'a'.repeat(50);
|
||||||
|
expect(sanitizeString(long)).toHaveLength(29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
expect(sanitizeString('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── formatBytes ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('formatBytes', () => {
|
||||||
|
it('returns "0 Bytes" for zero', () => {
|
||||||
|
expect(formatBytes(0)).toBe('0 Bytes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats bytes correctly', () => {
|
||||||
|
expect(formatBytes(500)).toBe('500.00 Bytes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats kilobytes correctly', () => {
|
||||||
|
expect(formatBytes(1024)).toBe('1.00 KB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats megabytes correctly', () => {
|
||||||
|
expect(formatBytes(1048576)).toBe('1.00 MB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects decimals parameter', () => {
|
||||||
|
expect(formatBytes(1500, 0)).toBe('1 KB');
|
||||||
|
expect(formatBytes(1500, 1)).toBe('1.5 KB');
|
||||||
|
});
|
||||||
|
});
|
||||||
82
tests/setup.js
Normal file
82
tests/setup.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Global mock for Thunderbird's browser.* WebExtension APIs
|
||||||
|
const browser = {
|
||||||
|
i18n: {
|
||||||
|
getMessage: vi.fn((key) => `[${key}]`),
|
||||||
|
getUILanguage: vi.fn(() => 'en-US'),
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
list: vi.fn(async () => []),
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
sync: {
|
||||||
|
get: vi.fn(async (defaults) => {
|
||||||
|
if (typeof defaults === 'object' && defaults !== null) {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
set: vi.fn(async () => {}),
|
||||||
|
remove: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
get: vi.fn(async (defaults) => {
|
||||||
|
if (typeof defaults === 'object' && defaults !== null) {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
set: vi.fn(async () => {}),
|
||||||
|
remove: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
getFull: vi.fn(async () => ({ headers: {}, parts: [] })),
|
||||||
|
get: vi.fn(async () => ({ tags: [] })),
|
||||||
|
tags: {
|
||||||
|
list: vi.fn(async () => []),
|
||||||
|
create: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
update: vi.fn(async () => {}),
|
||||||
|
listAttachments: vi.fn(async () => []),
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: vi.fn(async () => []),
|
||||||
|
create: vi.fn(async () => ({})),
|
||||||
|
update: vi.fn(async () => ({})),
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
getComposeDetails: vi.fn(async () => ({ body: '', subject: '' })),
|
||||||
|
setComposeDetails: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
contains: vi.fn(async () => false),
|
||||||
|
request: vi.fn(async () => true),
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
getURL: vi.fn((path) => `moz-extension://fake-id/${path}`),
|
||||||
|
sendMessage: vi.fn(async () => null),
|
||||||
|
},
|
||||||
|
mailTabs: {
|
||||||
|
getSelectedMessages: vi.fn(async () => ({ messages: [{ subject: '' }] })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global mock for Thunderbird's messenger.* APIs
|
||||||
|
const messenger = {
|
||||||
|
messageDisplay: {
|
||||||
|
getDisplayedMessage: vi.fn(async () => ({ subject: '' })),
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
getComposeDetails: vi.fn(async () => ({ body: '' })),
|
||||||
|
setComposeDetails: vi.fn(async () => {}),
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
continueList: vi.fn(async () => ({ messages: [] })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach to globalThis so module-level code can access them
|
||||||
|
globalThis.browser = browser;
|
||||||
|
globalThis.messenger = messenger;
|
||||||
21
vitest.config.js
Normal file
21
vitest.config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./tests/setup.js'],
|
||||||
|
include: ['tests/**/*.test.js'],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// js/mzta-prompts.js imports from "../../options/mzta-options-default.js"
|
||||||
|
// which resolves outside the repo root in Node.js. This alias fixes it.
|
||||||
|
'../../options/mzta-options-default.js': path.resolve(__dirname, 'options/mzta-options-default.js'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue