import { MdPos, ToastMark } from '@toast-ui/toastmark'; import MarkdownPreview, { CLASS_HIGHLIGHT } from '@/markdown/mdPreview'; import MarkdownEditor from '@/markdown/mdEditor'; import EventEmitter from '@/event/eventEmitter'; import * as sanitizer from '@/sanitizer/htmlSanitizer'; import { createHTMLrenderer, removeDataAttr } from './util'; function getHTML(preview: MarkdownPreview) { return removeDataAttr(preview.getHTML()); } jest.useFakeTimers(); describe('Preview', () => { let eventEmitter: EventEmitter, preview: MarkdownPreview; beforeEach(() => { jest.spyOn(sanitizer, 'sanitizeHTML'); const options = { linkAttributes: null, customHTMLRenderer: {}, isViewer: false, highlight: true, sanitizer: sanitizer.sanitizeHTML, }; eventEmitter = new EventEmitter(); preview = new MarkdownPreview(eventEmitter, options); }); afterEach(() => { jest.restoreAllMocks(); preview.destroy(); }); it('listen to updatePreview and update the preview', () => { const doc = new ToastMark(); const editResult = doc.editMarkdown([1, 7], [1, 7], 'changed'); eventEmitter.emit('updatePreview', editResult); expect(getHTML(preview)).toBe('

changed

'); }); it('should call sanitizeHTML', () => { const doc = new ToastMark(); const editResult = doc.editMarkdown( [1, 1], [1, 1], `` ); eventEmitter.emit('updatePreview', editResult); expect(sanitizer.sanitizeHTML).toHaveBeenCalledTimes(1); }); }); describe('preview highlight', () => { let eventEmitter: EventEmitter, preview: MarkdownPreview, editor: MarkdownEditor, editorEl: HTMLElement; function init(highlight: boolean) { const options = { linkAttributes: null, customHTMLRenderer: {}, isViewer: false, highlight, sanitizer: sanitizer.sanitizeHTML, }; eventEmitter = new EventEmitter(); editor = new MarkdownEditor(eventEmitter, { toastMark: new ToastMark() }); preview = new MarkdownPreview(eventEmitter, options); editorEl = editor.getElement(); document.body.appendChild(editorEl); document.body.appendChild(preview.getElement()!); } function setMarkdown(markdown: string) { editor.setMarkdown(markdown); } function setCursor(caret: MdPos) { editor.setSelection(caret, caret); } function blur() { editor.blur(); } function getHighlightedCount() { return preview.el!.querySelectorAll(`.${CLASS_HIGHLIGHT}`).length; } function assertHighlighted(tagName: string, html: string) { const el = preview.el!.querySelector(`.${CLASS_HIGHLIGHT}`)!; expect(el.tagName).toBe(tagName); expect(el.innerHTML).toBe(html); } afterEach(() => { jest.clearAllTimers(); document.body.removeChild(editorEl); editor.destroy(); preview.destroy(); }); it('highlighted element should be one', () => { init(true); setMarkdown('# Hello\n\nWorld'); setCursor([1, 1]); expect(getHighlightedCount()).toBe(1); assertHighlighted('H1', 'Hello'); setCursor([3, 1]); expect(getHighlightedCount()).toBe(1); assertHighlighted('P', 'World'); }); it('highlighted element is not displayed when highlight option is false', () => { init(false); setMarkdown('# Hello\n\nWorld'); setCursor([1, 1]); expect(getHighlightedCount()).toBe(0); setCursor([3, 1]); expect(getHighlightedCount()).toBe(0); }); it('paragraph inside tight list item should not be removed', () => { init(true); setMarkdown('- Item1\n- Item2'); setCursor([1, 4]); expect(assertHighlighted('P', 'Item1')); setCursor([2, 4]); expect(assertHighlighted('P', 'Item2')); }); describe('table cell', () => { beforeEach(() => { init(true); setMarkdown('| a | b |\n| - | - |\n| c | d |\n\n'); }); it('whitespace and delimiter should be considered as a table cell', () => { setCursor([1, 2]); assertHighlighted('TH', 'a'); setCursor([1, 5]); assertHighlighted('TH', 'a'); setCursor([1, 6]); assertHighlighted('TH', 'b'); setCursor([1, 8]); assertHighlighted('TH', 'b'); setCursor([3, 1]); assertHighlighted('TD', 'c'); setCursor([3, 5]); assertHighlighted('TD', 'c'); setCursor([3, 6]); assertHighlighted('TD', 'd'); setCursor([3, 8]); assertHighlighted('TD', 'd'); }); it('delimiter row should not highlight any element', () => { setCursor([2, 2]); expect(getHighlightedCount()).toBe(0); setCursor([2, 4]); expect(getHighlightedCount()).toBe(0); setCursor([2, 6]); expect(getHighlightedCount()).toBe(0); }); it('empty line next to table should not highlight any element ', () => { setCursor([4, 1]); expect(getHighlightedCount()).toBe(0); }); }); it('the highlighted element disappears when blur event is triggered', () => { init(true); setMarkdown('# Heading'); setCursor([1, 1]); // run setTimeout function when focusing the editor jest.runAllTimers(); expect(getHighlightedCount()).toBe(1); blur(); expect(getHighlightedCount()).toBe(0); }); }); describe('Preview with html renderer', () => { let eventEmitter: EventEmitter, preview: MarkdownPreview; function createPreviewWithHTMLRenderer() { const options = { linkAttributes: null, customHTMLRenderer: createHTMLrenderer(), isViewer: false, highlight: true, sanitizer: sanitizer.sanitizeHTML, }; sanitizer.registerTagWhitelistIfPossible('iframe'); eventEmitter = new EventEmitter(); preview = new MarkdownPreview(eventEmitter, options); } beforeEach(() => { createPreviewWithHTMLRenderer(); }); it('should render iframe node to preview ignoring sanitizer tag', () => { const doc = new ToastMark(); const editResult = doc.editMarkdown( [1, 1], [1, 1], '' ); eventEmitter.emit('updatePreview', editResult); expect(getHTML(preview)).toBe( '' ); }); it('should render html inline node', () => { const doc = new ToastMark(); const editResult = doc.editMarkdown([1, 1], [1, 1], 'content'); eventEmitter.emit('updatePreview', editResult); expect(getHTML(preview)).toBe('

content

'); }); });