import { describe, it, expect } from 'vitest'; import DOMPurify from 'dompurify'; import { EMAIL_IFRAME_SANITIZE_CONFIG, applyNewTabToAnchor, plainTextToSafeHtml, sanitizePlainTextRenderedHtml, parseHtmlSafely, } from '../email-sanitization'; /** * Regression guard for "email links open in a new tab". The at-risk links are * generated by linkification (not in the source) and DOMPurify was silently * stripping their target/rel, so they opened in the same tab. Drives both real * EmailViewer pipelines end-to-end — plaintext and HTML/iframe — so it can't * regress unnoticed. */ /** Reproduce the EmailViewer iframe pipeline for an HTML body. */ function renderIframeHtml(html: string): Document { DOMPurify.addHook('afterSanitizeAttributes', applyNewTabToAnchor); let clean: string; try { clean = DOMPurify.sanitize(html, EMAIL_IFRAME_SANITIZE_CONFIG); } finally { DOMPurify.removeAllHooks(); } const doc = parseHtmlSafely(clean); // Post-render walk, exactly as handleIframeLoad does on the live iframe doc. doc.querySelectorAll('a').forEach(applyNewTabToAnchor); return doc; } /** Reproduce the EmailViewer plaintext pipeline (rendered into the main DOM). */ function renderPlaintext(text: string): Document { return parseHtmlSafely(sanitizePlainTextRenderedHtml(plainTextToSafeHtml(text))); } const findLink = (doc: Document, hrefIncludes: string): HTMLAnchorElement | undefined => Array.from(doc.querySelectorAll('a')).find((a) => (a.getAttribute('href') || '').includes(hrefIncludes)); describe('email link new-tab behaviour (integration)', () => { describe('plaintext body (links are generated, not in the source)', () => { it('opens an http(s) URL in a new tab with noopener noreferrer', () => { const doc = renderPlaintext('Please visit https://example.com/welcome today.'); const link = findLink(doc, 'example.com'); expect(link).toBeTruthy(); expect(link!.getAttribute('href')).toBe('https://example.com/welcome'); expect(link!.getAttribute('target')).toBe('_blank'); expect(link!.getAttribute('rel')).toBe('noopener noreferrer'); }); it('does not turn a bare email address into a new-tab link', () => { const doc = renderPlaintext('Write to foo@bar.com for help.'); // plaintext linkification only targets http(s) URLs, never mailto. expect(doc.querySelectorAll('a').length).toBe(0); }); }); describe('HTML alternative that looks like plaintext (server-generated tags)', () => { it('opens http(s) anchors in a new tab and adds noopener noreferrer', () => { const doc = renderIframeHtml('Hi
http://example.org/page
Bye'); const link = findLink(doc, 'example.org'); expect(link!.getAttribute('target')).toBe('_blank'); expect(link!.getAttribute('rel')).toBe('noopener noreferrer'); }); it('does NOT add target=_blank to mailto links', () => { const doc = renderIframeHtml('sales@example.com'); const link = findLink(doc, 'mailto:'); expect(link).toBeTruthy(); expect(link!.getAttribute('target')).toBeNull(); expect(link!.getAttribute('rel')).toBeNull(); }); it('does NOT add target=_blank to in-page #anchors', () => { const doc = renderIframeHtml('jump'); const link = findLink(doc, '#section'); expect(link!.getAttribute('target')).toBeNull(); }); it('strips an author-supplied target=_blank from a mailto link', () => { const doc = renderIframeHtml('x'); const link = findLink(doc, 'mailto:'); expect(link!.getAttribute('target')).toBeNull(); }); it('handles a mixed body: http gets a new tab, mailto does not', () => { const doc = renderIframeHtml( 'See docs or mail us.', ); expect(findLink(doc, 'docs.example.com')!.getAttribute('target')).toBe('_blank'); expect(findLink(doc, 'mailto:')!.getAttribute('target')).toBeNull(); }); }); });