import { describe, it, expect } from 'vitest'; import { buildInitialHtml, htmlToPlainText, rewriteInlineImages, extractUserAuthoredText, rewriteCidImagesForEditor, replaceInlineImagePlaceholders, } from '../compose-html'; describe('buildInitialHtml', () => { it('returns empty paragraph for plain compose', () => { expect(buildInitialHtml('compose')).toBe('


'); }); it('returns empty paragraph when reply has no body', () => { expect(buildInitialHtml('reply', { from: { email: 'a@b.com' } })).toBe('


'); }); it('builds reply with quoted plain-text body and sender label', () => { const html = buildInitialHtml('reply', { from: { name: 'Alice', email: 'a@b.com' }, subject: 'Hello', body: 'line one\nline two', receivedAt: '2026-04-27T10:00:00Z', }); expect(html).toContain('


'); // Header shows the full sender incl. address, escaped (webmail #482). expect(html).toMatch(/On .+, Alice <a@b.com> wrote:/); expect(html).toContain('data-quoted-html'); expect(html).toContain('line two'); }); it('falls back to the email address when the sender has no name', () => { const html = buildInitialHtml('reply', { from: { email: 'a@b.com' }, body: 'hi', receivedAt: '2026-04-27T10:00:00Z', }); expect(html).toContain('a@b.com wrote:'); }); it('uses HTML body when present, stripping dangerous tags', () => { const html = buildInitialHtml('reply', { from: { email: 'x@y.com' }, body: 'fallback', htmlBody: '

safe

', }); expect(html).toContain('

safe

'); expect(html).not.toContain(' { const html = buildInitialHtml('forward', { from: { name: 'Bob', email: 'b@c.com' }, subject: 'Original Subject', body: 'forwarded body', receivedAt: '2026-04-27T10:00:00Z', }); expect(html).toContain('Forwarded message'); expect(html).toContain('From: Bob <b@c.com>'); expect(html).toContain('Subject: Original Subject'); expect(html).toContain('forwarded body'); }); it('escapes HTML in plain-text quoted body', () => { const html = buildInitialHtml('reply', { from: { email: 'a@b.com' }, body: '', }); expect(html).not.toContain(''); expect(html).toContain('<script>x</script>'); }); }); describe('htmlToPlainText', () => { it('returns empty string for empty input', () => { expect(htmlToPlainText('')).toBe(''); }); it('strips tags and decodes entities', () => { expect(htmlToPlainText('

hello & world

')).toBe('hello & world'); }); it('converts
to newline', () => { expect(htmlToPlainText('a
b
c')).toBe('a\nb\nc'); }); it('treats block boundaries as newlines', () => { expect(htmlToPlainText('

a

b

')).toBe('a\n\nb'); }); it('renders list items with bullet prefix', () => { expect(htmlToPlainText('')).toBe('• one\n• two'); }); it('collapses excessive blank lines', () => { expect(htmlToPlainText('

a




b

')).toBe('a\n\nb'); }); it('decodes   to space', () => { expect(htmlToPlainText('hello world')).toBe('hello world'); }); }); describe('rewriteInlineImages', () => { it('returns input unchanged when no data-cid is present', () => { const html = '

hi

'; const out = rewriteInlineImages(html); expect(out.html).toBe(html); expect(out.usedCids).toEqual([]); }); it('rewrites data-cid images to cid: src and reports the CID', () => { const html = '

pic

'; const out = rewriteInlineImages(html); expect(out.html).toContain('src="cid:abc@x"'); expect(out.html).not.toContain('data-cid'); expect(out.html).not.toContain('data:image/png'); expect(out.usedCids).toEqual(['abc@x']); }); it('handles single-quoted attributes', () => { const html = `x`; const out = rewriteInlineImages(html); expect(out.html).toContain('src="cid:zzz"'); expect(out.usedCids).toEqual(['zzz']); }); it('preserves alt attribute on rewritten images', () => { const html = 'my pic'; const out = rewriteInlineImages(html); expect(out.html).toContain('alt="my pic"'); }); it('reports each cid only once even if image referenced multiple times', () => { const html = ''; const out = rewriteInlineImages(html); expect(out.usedCids).toEqual(['dup']); }); }); describe('htmlToPlainText links', () => { it('renders a link as text ', () => { expect(htmlToPlainText('

see docs

')).toBe('see docs '); }); it('keeps a bare URL link as the URL only', () => { expect(htmlToPlainText('https://x.y')).toBe('https://x.y'); }); it('drops the mailto: prefix when the text is the address', () => { expect(htmlToPlainText('a@b.c')).toBe('a@b.c'); }); }); describe('extractUserAuthoredText', () => { it('drops the quoted island and blockquotes', () => { const html = buildInitialHtml('reply', { from: { name: 'Alice', email: 'a@b.com' }, body: 'please find attached the invoice', receivedAt: '2026-04-27T10:00:00Z', }); const text = extractUserAuthoredText('

thanks

' + html); expect(text).toBe('thanks'); }); it('cuts everything from the forwarded separator', () => { const text = extractUserAuthoredText( '

fyi

---------- Forwarded message ----------
From: x

attached

', { forwardedSeparator: '---------- Forwarded message ----------' }, ); expect(text.trim()).toBe('fyi'); }); it('drops quoted lines in plain-text mode', () => { expect(extractUserAuthoredText('mine\n> theirs attached', { plainTextMode: true })).toBe('mine'); }); }); describe('cid image hydration', () => { it('rewrites cid: images to placeholders with data-cid and back to data URLs', () => { const { html, cids } = rewriteCidImagesForEditor('

p

'); expect(cids).toEqual(['pic@x']); expect(html).toContain('data-cid="pic@x"'); expect(html).not.toContain('cid:pic@x'); const hydrated = replaceInlineImagePlaceholders(html, new Map([['pic@x', 'data:image/png;base64,AAA']])); expect(hydrated).toContain('src="data:image/png;base64,AAA"'); expect(hydrated).toContain('data-cid="pic@x"'); expect(rewriteInlineImages(hydrated).html).toContain('src="cid:pic@x"'); }); // The composer builds the URI from the part's type, which the server // returns verbatim. it('leaves the placeholder when the data URL is not an image type plus base64', () => { const { html } = rewriteCidImagesForEditor('

'); const hostile = 'data:image/x">;base64,QUJD'; const hydrated = replaceInlineImagePlaceholders(html, new Map([['pic@x', hostile]])); expect(hydrated).toBe(html); expect(hydrated).not.toContain('