import '@/i18n/en-us'; import { oneLineTrim, stripIndents, source } from 'common-tags'; import { Emitter } from '@t/event'; import { EditorOptions } from '@t/editor'; import type { OpenTagToken } from '@toast-ui/toastmark'; import i18n from '@/i18n/i18n'; import Editor from '@/editor'; import Viewer from '@/viewer'; import * as commonUtil from '@/utils/common'; import { createHTMLrenderer } from './markdown/util'; import { cls } from '@/utils/dom'; import * as imageHelper from '@/helper/image'; const HEADING_CLS = `${cls('md-heading')} ${cls('md-heading1')}`; const DELIM_CLS = cls('md-delimiter'); describe('editor', () => { let container: HTMLElement, mdEditor: HTMLElement, mdPreview: HTMLElement, wwEditor: HTMLElement, editor: Editor; function getPreviewHTML() { return mdPreview .querySelector(`.${cls('contents')}`)! .innerHTML.replace(/\sdata-nodeid="\d+"|\n/g, '') .trim(); } describe('instance API', () => { beforeEach(() => { container = document.createElement('div'); editor = new Editor({ el: container, previewHighlight: false, widgetRules: [ { rule: /@\S+/, toDOM(text) { const span = document.createElement('span'); span.innerHTML = `${text}`; return span; }, }, ], }); const elements = editor.getEditorElements(); mdEditor = elements.mdEditor; mdPreview = elements.mdPreview!; wwEditor = elements.wwEditor!; document.body.appendChild(container); }); afterEach(() => { editor.destroy(); document.body.removeChild(container); }); describe('convertPosToMatchEditorMode', () => { const mdPos: [number, number] = [2, 1]; const wwPos = 14; it('should convert position to match editor mode', () => { editor.setMarkdown('Hello World\nwelcome to the world'); editor.changeMode('wysiwyg'); expect(editor.convertPosToMatchEditorMode(mdPos)).toEqual([wwPos, wwPos]); editor.changeMode('markdown'); expect(editor.convertPosToMatchEditorMode(wwPos)).toEqual([mdPos, mdPos]); }); it('should occurs error when types of parameters is not matched', () => { expect(() => { editor.convertPosToMatchEditorMode(mdPos, wwPos); }).toThrowError(); }); }); it('setPlaceholder()', () => { editor.setPlaceholder('Please input text'); const expected = ''; expect(mdEditor).toContainHTML(expected); expect(wwEditor).toContainHTML(expected); }); describe('getHTML()', () => { it('basic', () => { editor.setMarkdown('# heading\n* bullet'); const result = oneLineTrim`
bullet
first line
second line
\nthird line
\n
\nfourth line
first line
second line
third line
fourth line
`; editor.setHTML(input); expect(editor.getHTML()).toBe(expected); }); it('placeholder should be removed', () => { editor.changeMode('wysiwyg'); editor.setPlaceholder('placeholder'); const result = oneLineTrim`CRLF
'); }); }); describe('setHTML()', () => { it('basic', () => { editor.setHTML('a
b
a
b
test bold
test italic
normal text
test bold
test italic
normal text
`; expect(wwEditor).toContainHTML(expected); }); it('should parse the br tag with the paragraph block to separate between blocks', () => { const input = source`first line
second line
\nthird line
\n
\nfourth line
first line
second line
third line
fourth line
first line
second line
\nthird line
\n
\nfourth line
`; expect(mdEditor).toContainHTML(expectedEditor); expect(getPreviewHTML()).toBe(expectedPreview); }); it('in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.replaceWithWidget(1, 1, '@test'); const expected = oneLineTrim` `; expect(wwEditor).toContainHTML(expected); }); }); it('exec()', () => { // @ts-ignore jest.spyOn(editor.commandManager, 'exec'); editor.exec('bold'); // @ts-ignore // eslint-disable-next-line no-undefined expect(editor.commandManager.exec).toHaveBeenCalledWith('bold', undefined); }); it('addCommand()', () => { const spy = jest.fn(); // @ts-ignore const { view } = editor.mdEditor; const { state, dispatch } = view; editor.addCommand('markdown', 'custom', spy); editor.exec('custom', { prop: 'prop' }); expect(spy).toHaveBeenCalledWith({ prop: 'prop' }, state, dispatch, view); expect(spy).toHaveBeenCalled(); }); it('should be triggered only once when the event registered by addHook()', () => { const spy = jest.fn(); const { eventEmitter } = editor; eventEmitter.addEventType('custom'); editor.addHook('custom', spy); editor.addHook('custom', spy); eventEmitter.emit('custom'); expect(spy).toHaveBeenCalledTimes(1); }); describe('insertText()', () => { it('in markdown', () => { editor.insertText('test'); expect(mdEditor).toContainHTML('
test
'); }); it('in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.insertText('test'); expect(wwEditor).toContainHTML('test
'); }); }); describe('setSelection(), getSelection()', () => { it('in markdown', () => { expect(editor.getSelection()).toEqual([ [1, 1], [1, 1], ]); editor.setMarkdown('line1\nline2'); editor.setSelection([1, 2], [2, 4]); expect(editor.getSelection()).toEqual([ [1, 2], [2, 4], ]); }); it('in wysiwyg', () => { editor.changeMode('wysiwyg'); expect(editor.getSelection()).toEqual([1, 1]); editor.setMarkdown('line1\nline2'); editor.setSelection(2, 8); expect(editor.getSelection()).toEqual([2, 8]); }); }); describe('getSelectedText()', () => { beforeEach(() => { editor.setMarkdown('line1\nline2'); editor.setSelection([1, 2], [2, 4]); }); it('in markdown', () => { expect(editor.getSelectedText()).toEqual('ine1\nlin'); expect(editor.getSelectedText([1, 2], [2, 6])).toEqual('ine1\nline2'); }); it('in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.setSelection(2, 11); expect(editor.getSelectedText()).toEqual('ine1\nlin'); expect(editor.getSelectedText(2, 13)).toEqual('ine1\nline2'); }); }); describe('replaceSelection()', () => { beforeEach(() => { editor.setMarkdown('line1\nline2'); editor.setSelection([1, 2], [2, 4]); }); it('should replace current selection in markdown', () => { editor.replaceSelection('Replaced'); expect(mdEditor).toContainHTML('lReplacede2
'); }); it('should replace current selection in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.setSelection(2, 11); editor.replaceSelection('Replaced'); expect(wwEditor).toContainHTML('lReplacede2
'); }); it('should replace given selection in markdown', () => { editor.replaceSelection('Replaced', [1, 1], [2, 1]); expect(mdEditor).toContainHTML('Replacedline2
'); }); it('should replace given selection in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.replaceSelection('Replaced', 1, 7); expect(wwEditor).toContainHTML('Replaced
line2
'); }); it('should parse the CRLF properly in markdown', () => { editor.replaceSelection('text\r\nCRLF'); expect(mdEditor).toContainHTML('ltext
CRLFe2
le2
'); }); it('should delete current selection in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.setSelection(2, 11); editor.deleteSelection(); expect(wwEditor).toContainHTML('le2
'); }); it('should delete given selection in markdown', () => { editor.deleteSelection([1, 1], [2, 1]); expect(mdEditor).toContainHTML('line2
'); }); it('should delete given selection in wysiwyg', () => { editor.changeMode('wysiwyg'); editor.deleteSelection(1, 7); expect(wwEditor).toContainHTML('line2
'); }); }); describe('getRangeOfNode()', () => { beforeEach(() => { editor.setMarkdown('line1\nline2 **strong**'); editor.setSelection([2, 10], [2, 12]); }); it('should get the range of the current selected node in markdown', () => { const rangeInfo = editor.getRangeInfoOfNode(); const [start, end] = rangeInfo.range; expect(rangeInfo).toEqual({ range: [ [2, 7], [2, 17], ], type: 'strong', }); editor.replaceSelection('Replaced', start, end); expect(getPreviewHTML()).toBe('line1
line2 Replaced
line1
line2 Replaced
'); }); it('should get the range of selection with given position in markdown', () => { const rangeInfo = editor.getRangeInfoOfNode([2, 2]); const [start, end] = rangeInfo.range; expect(rangeInfo).toEqual({ range: [ [2, 1], [2, 7], ], type: 'text', }); editor.replaceSelection('Replaced', start, end); expect(getPreviewHTML()).toBe('line1
Replacedstrong
line1
Replacedstrong
'); }); }); }); describe('static API', () => { it('factory()', () => { const editorInst = Editor.factory({ el: document.createElement('div'), viewer: false }); const viewerInst = Editor.factory({ el: document.createElement('div'), viewer: true }); expect(editorInst).toBeInstanceOf(Editor); expect(viewerInst).toBeInstanceOf(Viewer); }); it('setLanguage()', () => { const data = {}; jest.spyOn(i18n, 'setLanguage'); Editor.setLanguage('ko', data); expect(i18n.setLanguage).toHaveBeenCalledWith('ko', data); }); }); describe('options', () => { beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { editor.destroy(); document.body.removeChild(container); }); function createEditor(options: EditorOptions) { editor = new Editor(options); const elements = editor.getEditorElements(); mdEditor = elements.mdEditor; mdPreview = elements.mdPreview!; wwEditor = elements.wwEditor!; } describe('plugins', () => { it('should invoke plugin functions', () => { const fooPlugin = jest.fn().mockReturnValue({}); const barPlugin = jest.fn().mockReturnValue({}); createEditor({ el: container, plugins: [fooPlugin, barPlugin] }); // @ts-ignore const { eventEmitter } = editor; expect(fooPlugin).toHaveBeenCalledWith(expect.objectContaining({ eventEmitter })); expect(barPlugin).toHaveBeenCalledWith(expect.objectContaining({ eventEmitter })); }); it('should invoke plugin function with options of plugin', () => { const plugin = jest.fn().mockReturnValue({}); const options = {}; createEditor({ el: container, plugins: [[plugin, options]] }); // @ts-ignore const { eventEmitter } = editor; expect(plugin).toHaveBeenCalledWith( expect.objectContaining({ eventEmitter }), expect.objectContaining(options) ); }); it(`should add command to command manager when plugin return 'markdownCommands' value`, () => { const spy = jest.fn(); const plugin = () => { return { markdownCommands: { foo: () => { spy(); return true; }, }, }; }; createEditor({ el: container, plugins: [plugin] }); editor.exec('foo'); expect(spy).toHaveBeenCalled(); }); it(`should add command to command manager when plugin return 'wysiwygCommands' value`, () => { const spy = jest.fn(); const plugin = () => { return { wysiwygCommands: { foo: () => { spy(); return true; }, }, }; }; createEditor({ el: container, plugins: [plugin] }); editor.changeMode('wysiwyg'); editor.exec('foo'); expect(spy).toHaveBeenCalled(); }); it(`should add toolbar item when plugin return 'toolbarItems' value`, () => { const toolbarItem = { name: 'color', tooltip: 'Text color', className: 'toastui-editor-toolbar-icons color', }; const plugin = () => { return { toolbarItems: [{ groupIndex: 1, itemIndex: 2, item: toolbarItem }], }; }; createEditor({ el: container, plugins: [plugin] }); const toolbar = document.querySelector(`.${cls('toolbar-icons.color')}`); expect(toolbar).toBeInTheDocument(); }); }); describe('usageStatistics', () => { it('should send request hostname in payload by default', () => { spyOn(commonUtil, 'sendHostName'); createEditor({ el: container }); expect(commonUtil.sendHostName).toHaveBeenCalled(); }); it('should not send request if the option is set to false', () => { spyOn(commonUtil, 'sendHostName'); createEditor({ el: container, usageStatistics: false }); expect(commonUtil.sendHostName).not.toHaveBeenCalled(); }); }); describe('hideModeSwitch', () => { it('should hide mode switch if the option value is true', () => { createEditor({ el: container, hideModeSwitch: true }); const modeSwitch = document.querySelector(`.${cls('mode-switch')}`); expect(modeSwitch).not.toBeInTheDocument(); }); }); describe('extendedAutolinks option', () => { it('should convert url-like strings to anchor tags', () => { createEditor({ el: container, initialValue: 'http://nhn.com', extendedAutolinks: true, previewHighlight: false, }); expect(getPreviewHTML()).toBe(''); }); }); describe('disallowDeepHeading internal parsing option', () => { it('should disallow the nested seTextHeading in list', () => { createEditor({ el: container, initialValue: '- item1\n\t-', previewHighlight: false, }); const result = oneLineTrim`item1
-
# item1
`; expect(getPreviewHTML()).toBe(result); }); it('should disallow the nested atxHeading in blockquote', () => { createEditor({ el: container, initialValue: '> # item1', previewHighlight: false, }); const result = oneLineTrim`item1
-
`; expect(getPreviewHTML()).toBe(result); }); }); describe('frontMatter option', () => { it('should parse the front matter as the paragraph in WYSIWYG', () => { createEditor({ el: container, frontMatter: true, initialValue: '---\ntitle: front matter\n---', initialEditType: 'wysiwyg', }); const result = stripIndents`# item1
Hello World
'); }); it('linkAttributes option should be applied to original renderer', () => { createEditor({ el: container, initialValue: '[Hello](nhn.com)', linkAttributes: { target: '_blank' }, previewHighlight: false, customHTMLRenderer: { link(_, { origin }) { return origin!(); }, }, }); expect(getPreviewHTML()).toBe(''); }); it('should render html block node regardless of the sanitizer', () => { createEditor({ el: container, initialValue: '\n\ntest', previewHighlight: false, // add iframe html block renderer customHTMLRenderer: createHTMLrenderer(), }); const result = oneLineTrim`test
`; expect(getPreviewHTML()).toBe(result); }); it('should keep the html block node after changing the mode', () => { createEditor({ el: container, initialValue: '\n\ntest', previewHighlight: false, // add iframe html block renderer customHTMLRenderer: createHTMLrenderer(), }); editor.changeMode('wysiwyg'); const result = oneLineTrim`test
`; expect(wwEditor.innerHTML).toContain(result); }); it('should keep the html attributes with an empty string after changing the mode', () => { createEditor({ el: container, initialValue: '', previewHighlight: false, // add iframe html block renderer customHTMLRenderer: createHTMLrenderer(), }); editor.changeMode('wysiwyg'); const result = oneLineTrim` `; expect(wwEditor.innerHTML).toContain(result); }); }); describe('hooks option', () => { const defaultImageBlobHookSpy = jest.fn(); function mockDefaultImageBlobHook() { defaultImageBlobHookSpy.mockReset(); jest .spyOn(imageHelper, 'addDefaultImageBlobHook') .mockImplementation((emitter: Emitter) => { emitter.listen('addImageBlobHook', defaultImageBlobHookSpy); }); } it('should remove default `addImageBlobHook` event handler after registering hook', () => { const spy = jest.fn(); mockDefaultImageBlobHook(); createEditor({ el: container, hooks: { addImageBlobHook: spy, }, }); editor.eventEmitter.emit('addImageBlobHook'); expect(spy).toHaveBeenCalled(); expect(defaultImageBlobHookSpy).not.toHaveBeenCalled(); }); }); }); });