import { describe, it, expect } from 'vitest'; import { buildEmbeddedSignatureHtml, htmlComposeBodyToPlainText, plainComposeBodyToHtml, replaceEmbeddedSignature, replacePlainTextSignature, replacePlainTextSignatureAboveQuote, stripEmbeddedSignature, } from '../compose-format'; import { SIGNATURE_BLOCK_MARKER, SIGNATURE_RANGE_MARKER, containsEmbeddedSignature } from '../signature-block'; import { plainTextBodyHasSignature } from '@/lib/signature-utils'; const identity = { htmlSignature: 'Alice
Acme', textSignature: 'Alice\nAcme' }; const count = (haystack: string, needle: string) => haystack.split(needle).length - 1; describe('compose-format (#1022): HTML <-> plain text switch', () => { describe('stripEmbeddedSignature', () => { it('removes the marker-bracketed range and reports it', () => { const html = `

Hi

${buildEmbeddedSignatureHtml(identity, { embed: true, separator: true })}`; const out = stripEmbeddedSignature(html); expect(out.hadSignature).toBe(true); expect(out.html).toBe('

Hi

'); }); it('stops before the quote when the end marker is missing', () => { const html = `

Hi

--

Alice

original
`; const out = stripEmbeddedSignature(html); expect(out.hadSignature).toBe(true); expect(out.html).toBe('

Hi

original
'); }); it('leaves a body without markers untouched', () => { expect(stripEmbeddedSignature('

Hi

')).toEqual({ html: '

Hi

', hadSignature: false }); }); }); describe('htmlComposeBodyToPlainText', () => { it('flattens formatting and carries the signature over as plain text', () => { const html = `

Hello there

Second

${buildEmbeddedSignatureHtml(identity, { embed: true, separator: true })}`; const text = htmlComposeBodyToPlainText(html, identity, { separator: true }); expect(text).toBe('Hello there\n\nSecond\n\n-- \nAlice\nAcme'); expect(plainTextBodyHasSignature(text, identity)).toBe(true); expect(count(text, 'Alice')).toBe(1); }); it('does not invent a signature for a body that had none (below-quote reply)', () => { const text = htmlComposeBodyToPlainText('

Thanks

original
', identity, { separator: true }); expect(text).toBe('Thanks\n\noriginal'); expect(plainTextBodyHasSignature(text, identity)).toBe(false); }); it('honours the separator setting', () => { const html = `

Hi

${buildEmbeddedSignatureHtml(identity, { embed: true, separator: false })}`; expect(htmlComposeBodyToPlainText(html, identity, { separator: false })).toBe('Hi\n\nAlice\nAcme'); }); }); describe('plainComposeBodyToHtml', () => { it('wraps paragraphs and re-embeds the signature with markers', () => { const html = plainComposeBodyToHtml('Hello\nthere\n\nSecond\n\n-- \nAlice\nAcme', identity, { separator: true }); expect(html.startsWith('

Hello
there

Second

')).toBe(true); expect(containsEmbeddedSignature(html)).toBe(true); expect(count(html, SIGNATURE_BLOCK_MARKER)).toBe(1); expect(html).toContain('Alice'); // The plain-text copy is gone - only the embedded HTML one remains. expect(count(html, 'Alice')).toBe(1); }); it('keeps a body without signature signature-free', () => { const html = plainComposeBodyToHtml('Thanks\n\n> original', identity, { separator: true }); expect(html).toBe('

Thanks

> original

'); expect(containsEmbeddedSignature(html)).toBe(false); }); it('gives an untouched signature-only body an empty paragraph to type into', () => { const html = plainComposeBodyToHtml('\n\n-- \nAlice\nAcme', identity, { separator: true }); expect(html.startsWith('

')).toBe(true); expect(containsEmbeddedSignature(html)).toBe(true); }); }); it('round-trips a compose body without duplicating or losing the signature', () => { const start = `

Draft text

${buildEmbeddedSignatureHtml(identity, { embed: true, separator: true })}`; const text = htmlComposeBodyToPlainText(start, identity, { separator: true }); const back = plainComposeBodyToHtml(text, identity, { separator: true }); expect(back).toBe(start); }); it('adds no separator paragraph in front of a signature that has its own delimiter', () => { const html = buildEmbeddedSignatureHtml({ textSignature: '-- \nUser A' }, { embed: true, separator: true }); expect(html).not.toContain(`${SIGNATURE_RANGE_MARKER}="separator"`); expect(count(html, '-- ')).toBe(1); }); }); describe('switching identity swaps the signature', () => { const other = { htmlSignature: 'Team', textSignature: 'Team' }; const none = { htmlSignature: '', textSignature: '' }; const embed = (sig: typeof identity) => buildEmbeddedSignatureHtml(sig, { embed: true, separator: true }); // The DOM writes valueless attributes as =""; compare in its spelling. const dom = (html: string) => new DOMParser().parseFromString(html, 'text/html').body.innerHTML; describe('replaceEmbeddedSignature', () => { it('puts the new signature where the old one was', () => { const html = `

Hi

${embed(identity)}
original
`; expect(replaceEmbeddedSignature(html, embed(other))) .toBe(dom(`

Hi

${embed(other)}
original
`)); }); it('removes the signature for an identity without one', () => { const html = `

Hi

${embed(identity)}
original
`; expect(replaceEmbeddedSignature(html, embed(none))).toBe('

Hi

original
'); }); it('adds a signature above the quote when the body had none', () => { expect(replaceEmbeddedSignature('

Hi

original
', embed(other))) .toBe(dom(`

Hi

${embed(other)}
original
`)); }); it('keeps text typed on the line under the signature', () => { const typedBelow = embed(identity).replace( `

`, `

P.S. see you

`, ); expect(replaceEmbeddedSignature(`

Hi

${typedBelow}`, embed(none))) .toBe('

Hi

P.S. see you

'); expect(replaceEmbeddedSignature(`

Hi

${typedBelow}`, embed(other))) .toBe(dom(`

Hi

${embed(other)}

P.S. see you

`)); }); it('adds a signature at the end when there is no quote', () => { expect(replaceEmbeddedSignature('

Hi

', embed(other))).toBe(dom(`

Hi

${embed(other)}`)); }); it('adds a signature above the quote header when the body had none (#842)', () => { const html = '

Hi

On Mon, Bob wrote:

original
'; expect(replaceEmbeddedSignature(html, embed(other), { quoteHeader: 'On Mon, Bob wrote:' })) .toBe(dom(`

Hi

${embed(other)}

On Mon, Bob wrote:

original
`)); }); it('adds a signature above a forward header that has no quote element', () => { const html = '

Hi

---------- Forwarded message ----------
From: Bob

original

'; expect(replaceEmbeddedSignature(html, embed(other), { quoteHeader: '---------- Forwarded message ----------' })) .toBe(dom(`

Hi

${embed(other)}

---------- Forwarded message ----------
From: Bob

original

`)); }); }); describe('replacePlainTextSignatureAboveQuote (#842)', () => { const header = 'On Mon, Bob wrote:'; const quote = `${header}\n> original`; it('adds the signature between the reply and the quote header', () => { expect(replacePlainTextSignatureAboveQuote(`\n\n${quote}`, header, none, identity, { separator: true })) .toBe(`\n\n-- \nAlice\nAcme\n\n${quote}`); expect(replacePlainTextSignatureAboveQuote(`Hi\n\n${quote}`, header, none, identity, { separator: true })) .toBe(`Hi\n\n-- \nAlice\nAcme\n\n${quote}`); }); it('swaps or removes the signature above the quote', () => { const signed = `Hi\n\n-- \nAlice\nAcme\n\n${quote}`; expect(replacePlainTextSignatureAboveQuote(signed, header, identity, other, { separator: true })) .toBe(`Hi\n\n-- \nTeam\n\n${quote}`); expect(replacePlainTextSignatureAboveQuote(signed, header, identity, none, { separator: true })) .toBe(`Hi\n\n${quote}`); }); it('leaves a body alone whose quote header is gone', () => { expect(replacePlainTextSignatureAboveQuote('Hi\n\n> original', header, none, identity, { separator: true })) .toBe('Hi\n\n> original'); }); }); describe('replacePlainTextSignature', () => { const signed = 'Hi\n\n-- \nAlice\nAcme'; it('swaps a trailing signature', () => { expect(replacePlainTextSignature(signed, identity, other, { separator: true })).toBe('Hi\n\n-- \nTeam'); }); it('removes it for an identity without one and adds it back', () => { const bare = replacePlainTextSignature(signed, identity, none, { separator: true }); expect(bare).toBe('Hi'); expect(replacePlainTextSignature(bare, none, identity, { separator: true })).toBe(signed); }); it('leaves a body alone whose signature the user edited', () => { const edited = 'Hi\n\n-- \nAlice (edited)'; expect(replacePlainTextSignature(edited, identity, other, { separator: true })).toBe(edited); }); }); });