Files

110 lines
5.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { prepareQuickReply, type QuickReplyQuestion, type QuickReplyQuestions } from '@/lib/outgoing/quick-reply';
import { registerOutgoingFinalizer, registerPreSendCheck, resetSendPipeline } from '@/lib/outgoing/send-pipeline';
import type { Identity } from '@/lib/jmap/types';
vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
// Quick reply goes through the same checks and finalisers as the composer,
// so the disclaimer and the external-recipient confirm reach it too.
const html = { id: 'i', name: 'Me', email: 'me@example.com', htmlSignature: '<b>Me</b>', textSignature: '' } as Identity;
const text = { id: 'i', name: 'Me', email: 'me@example.com', htmlSignature: '', textSignature: 'Me' } as Identity;
const input = (signatureIdentity: Identity) => ({
body: 'Thanks <3', signatureIdentity, separator: true, to: ['bob@example.com'], fromEmail: 'me@example.com', identityId: 'i',
});
afterEach(() => resetSendPipeline());
describe('prepareQuickReply', () => {
it('builds the text and, for an HTML signature, an escaped HTML body', async () => {
const withHtml = await prepareQuickReply(input(html));
expect(withHtml?.html).toContain('<div>Thanks &lt;3</div>');
expect(withHtml?.html).toContain('<b>Me</b>');
expect(withHtml?.text).toContain('Thanks <3');
const plain = await prepareQuickReply(input(text));
expect(plain?.html).toBeUndefined();
expect(plain?.text).toMatch(/Thanks <3[\s\S]*-- \nMe$/);
});
it('runs the finalisers with a reply context', async () => {
const contexts: unknown[] = [];
registerOutgoingFinalizer('f', 10, (bodies, context) => {
contexts.push(context);
return { html: bodies.html, text: `${bodies.text}\nFooter` };
});
const out = await prepareQuickReply(input(text));
expect(out?.text).toMatch(/\nFooter$/);
expect(contexts).toEqual([expect.objectContaining({ surface: 'quick-reply', mode: 'reply', to: ['bob@example.com'], plainText: true })]);
});
it('sends nothing when a check says no', async () => {
registerPreSendCheck('no', 10, () => false);
expect(await prepareQuickReply(input(text))).toBeNull();
});
});
// The composer warns about placeholders and forgotten attachments and asks
// "Send this message?" when that is on. Quick reply went out without any of
// them, an admin-enforced send confirmation included.
describe('prepareQuickReply warnings', () => {
const words = [{ before: ['your'], after: [], unfinished: ['TODO'] }];
const questions = (answer: boolean, overrides: Partial<QuickReplyQuestions> = {}) => {
const asked: QuickReplyQuestion[] = [];
return {
asked,
questions: {
placeholderWords: words,
attachmentKeywords: ['attached', 'anbei'],
confirmSend: false,
ask: async (q: QuickReplyQuestion) => { asked.push(q); return answer; },
...overrides,
} satisfies QuickReplyQuestions,
};
};
const reply = (body: string, q: QuickReplyQuestions) => prepareQuickReply({ ...input(text), body, questions: q });
it('asks about unfilled placeholders and stops on "back"', async () => {
const { asked, questions: q } = questions(false);
expect(await reply('See you at [TIME]. TODO', q)).toBeNull();
expect(asked).toEqual([{ kind: 'placeholders', found: ['[TIME]', 'TODO'] }]);
});
it('asks about a forgotten attachment and stops on "back"', async () => {
const { asked, questions: q } = questions(false);
expect(await reply('Die Datei ist anbei.', q)).toBeNull();
expect(asked).toEqual([{ kind: 'attachment', keyword: 'anbei' }]);
});
it('asks before sending when send confirmation is on, after the pre-send checks', async () => {
const order: string[] = [];
registerPreSendCheck('external', 10, () => { order.push('check'); return true; });
const { questions: q } = questions(false, { confirmSend: true });
const ask = async (question: QuickReplyQuestion) => { order.push(question.kind); return false; };
expect(await reply('Thanks!', { ...q, ask })).toBeNull();
expect(order).toEqual(['check', 'send']);
});
it('sends when every question is answered with "send anyway"', async () => {
const { asked, questions: q } = questions(true, { confirmSend: true });
const out = await reply('[TIME] works, file attached', q);
expect(out?.text).toContain('[TIME] works, file attached');
expect(asked.map((a) => a.kind)).toEqual(['placeholders', 'attachment', 'send']);
});
it('asks nothing for a plain reply, or when the warnings are off', async () => {
const { asked, questions: q } = questions(false);
expect(await reply('Thanks, see you then.', q)).not.toBeNull();
const off = questions(false, { placeholderWords: null, attachmentKeywords: null });
expect(await reply('[TIME], attached', off.questions)).not.toBeNull();
expect([...asked, ...off.asked]).toEqual([]);
});
it('leaves the signature out of the placeholder check', async () => {
const signed = { ...text, textSignature: 'Me\n[YOUR TITLE]' } as Identity;
const { asked, questions: q } = questions(false);
expect(await prepareQuickReply({ ...input(signed), body: 'Thanks!', questions: q })).not.toBeNull();
expect(asked).toEqual([]);
});
});