import { describe, expect, it, vi } from 'vitest'; import { htmlToPlainText } from '../html-to-text'; describe('htmlToPlainText', () => { it('preserves line breaks from
within a paragraph', () => { expect(htmlToPlainText('

Line 1.
Line 2.

')).toBe('Line 1.\nLine 2.'); }); it('separates block elements with newlines instead of running them together (#421)', () => { // The previous textContent-based implementation produced "Line 1.Line 2.Paragraph 2." expect(htmlToPlainText('
Line 1.
Line 2.
Paragraph 2.
')).toBe( 'Line 1.\nLine 2.\nParagraph 2.' ); }); it('with paragraphSpacing, separates paragraphs with a blank line', () => { expect( htmlToPlainText('

Line 1.
Line 2.

Paragraph 2.

', { paragraphSpacing: true }) ).toBe('Line 1.\nLine 2.\n\nParagraph 2.'); }); it('without paragraphSpacing, separates paragraphs with a single newline', () => { expect(htmlToPlainText('

A

B

')).toBe('A\nB'); }); it('renders links as text when the text equals the href', () => { expect( htmlToPlainText('

alice@example.com

') ).toBe('alice@example.com'); }); it('renders an @mention as just its text', () => { expect( htmlToPlainText('

Hi @Dana see us

') ).toBe("Hi @Dana see us "); }); it('renders links as "text " when they differ', () => { expect(htmlToPlainText('

Visit our site

')).toBe( 'Visit our site ' ); }); it('collapses excess whitespace and trims the result', () => { expect(htmlToPlainText('

Hello world

')).toBe('Hello world'); }); it('caps consecutive blank lines at one', () => { expect(htmlToPlainText('

A




B

', { paragraphSpacing: true })).toBe( 'A\n\nB' ); }); it('returns an empty string for empty or whitespace-only HTML', () => { expect(htmlToPlainText('')).toBe(''); expect(htmlToPlainText('

')).toBe(''); }); it('handles nested lists as separate lines', () => { expect(htmlToPlainText('
  • One
  • Two
')).toBe('One\nTwo'); }); it('keeps the trailing space of a "-- " signature delimiter', () => { // Without it the text part's delimiter was "--", which clients do not // recognise as the start of a signature. expect(htmlToPlainText('

Hi

--

User A

')).toBe('Hi\n-- \nUser A'); }); describe('mailMarkup', () => { // The text part of a reply showed the quoted original as ordinary text, // and list items without their bullets. const mail = { paragraphSpacing: true, mailMarkup: true }; it('prefixes quoted text with "> "', () => { const html = '

Sure.

On Sun, B wrote:

Hi A,

can you?

'; expect(htmlToPlainText(html, mail)).toBe('Sure.\n\nOn Sun, B wrote:\n\n> Hi A,\n>\n> can you?'); }); it('nests quotes', () => { expect(htmlToPlainText('

a

b

', mail)).toBe('> a\n>\n> > b'); }); // Every quote level used to serialise its innerHTML and parse it again, so // Reply on a long thread of nested quotes did quadratic work. it('parses a deeply nested quote once', () => { const depth = 300; const html = '
'.repeat(depth) + '

deep

' + '
'.repeat(depth); const parse = vi.spyOn(DOMParser.prototype, 'parseFromString'); try { expect(htmlToPlainText(html, mail)).toBe(`${'> '.repeat(depth)}deep`); expect(parse).toHaveBeenCalledTimes(1); } finally { parse.mockRestore(); } }); it('marks list items', () => { expect(htmlToPlainText('
  • Revenue
  • Costs
  1. x
  2. y
', mail)) .toBe('- Revenue\n- Costs\n\n3. x\n4. y'); }); it('leaves the default conversion unchanged', () => { expect(htmlToPlainText('
a
  • b
')).toBe('a\nb'); }); }); });