import { oneLineTrim, source, stripIndent } from 'common-tags'; import { redo, undo } from 'prosemirror-history'; import { chainCommands, deleteSelection, joinBackward, selectNodeBackward, } from 'prosemirror-commands'; import * as keymaps from 'prosemirror-keymap'; import { Sourcepos, ToastMark } from '@toast-ui/toastmark'; import MarkdownEditor from '@/markdown/mdEditor'; import MarkdownPreview from '@/markdown/mdPreview'; import EventEmitter from '@/event/eventEmitter'; import { sanitizeHTML } from '@/sanitizer/htmlSanitizer'; import { getTextContent, removeDataAttr, TestEditorWithNoneDelayHistory } from './util'; // @TODO: all tests should move to e2e test function forceKeymapFn(type: string, methodName: string, args: any[] = []) { const { specs, view } = mde; // @ts-ignore const [keymapFn] = specs.specs.filter((spec) => spec.name === type); // @ts-ignore keymapFn[methodName](...args)(view.state, view.dispatch); } function forceBackspaceKeymap() { const { state, dispatch } = mde.view; chainCommands(deleteSelection, joinBackward, selectNodeBackward)(state, dispatch, mde.view); } let mde: MarkdownEditor, em: EventEmitter, preview: MarkdownPreview; function getPreviewHTML() { return oneLineTrim`${removeDataAttr(preview.getHTML())}`; } function assertSelection(mdPos: Sourcepos) { expect(mde.getSelection()).toEqual(mdPos); } function execUndo() { const { state, dispatch } = mde.view; undo(state, dispatch); } beforeEach(() => { em = new EventEmitter(); mde = new TestEditorWithNoneDelayHistory(em, { toastMark: new ToastMark() }); const options = { linkAttributes: null, customHTMLRenderer: {}, isViewer: false, highlight: false, sanitizer: sanitizeHTML, }; preview = new MarkdownPreview(em, options); }); // @TODO: should add test case after developing the markdown editor API // describe('move table cell keymap', () => { // }); describe('extend table keymap', () => { it('should extend the table', () => { const input = source` | head1 | head2 | | --- | --- | | row1 | row1 | | row2 | row2 | `; const result = source` | head1 | head2 | | --- | --- | | row1 | row1 | | | | | row2 | row2 | `; mde.setMarkdown(input); mde.setSelection([3, 2], [3, 2]); forceKeymapFn('table', 'extendTable'); expect(getTextContent(mde)).toBe(result); }); it('should delete the row in case of empty table content', () => { const input = source` | head1 | head2 | | --- | --- | | row1 | row1 | | | | `; const result = source` | head1 | head2 | | --- | --- | | row1 | row1 | `; mde.setMarkdown(input); mde.setSelection([4, 2], [4, 2]); forceKeymapFn('table', 'extendTable'); expect(getTextContent(mde)).toBe(`${result}\n\n`); }); it('should not extend table list on multi line selection', () => { const input = source` | head1 | head2 | | --- | --- | | row1 | row1 | | row2 | row2 | `; mde.setMarkdown(input); mde.setSelection([2, 14], [4, 5]); forceKeymapFn('table', 'extendTable'); expect(getTextContent(mde)).toBe(input); }); it('should not extend the table out of table range', () => { const input = source` | head1 | head2 | | --- | --- | | row1 | row1 | | row2 | row2 | `; const result = source` | head1 | head2 | | --- | --- | | row1 | row1 | | row2 | row2 | `; mde.setMarkdown(input); mde.setSelection([4, 15], [4, 15]); forceKeymapFn('table', 'extendTable'); expect(getTextContent(mde)).toBe(result); }); it('should undo extend the table properly', () => { const input = source` | head1 | head2 | | --- | --- | | row1 | row1 | | row2 | row2 | text `; const result = oneLineTrim`
| head1 | head2 |
|---|---|
| row1 | row1 |
| row2 | row2 |
text
`; mde.setMarkdown(input); mde.setSelection([4, 2], [4, 2]); forceKeymapFn('table', 'extendTable'); execUndo(); expect(getPreviewHTML()).toBe(result); }); }); describe('extend block quote keymap', () => { it('should extend the block quote', () => { mde.setMarkdown('> block'); mde.setSelection([1, 8], [1, 8]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe('> block\n> '); assertSelection([ [2, 3], [2, 3], ]); }); it('should extend the block quote with sliced text', () => { mde.setMarkdown('> block'); mde.setSelection([1, 6], [1, 6]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe('> blo\n> ck'); assertSelection([ [2, 3], [2, 3], ]); }); it('should not extend the block quote on multi line selection', () => { const input = '> block1\n> block2'; mde.setMarkdown(input); mde.setSelection([1, 2], [2, 4]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe(input); }); it('should delete the row in case of empty block quote content', () => { mde.setMarkdown('> block\n> '); mde.setSelection([2, 2], [2, 2]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe('> block\n\n'); }); it('should delete the row in case of empty block quote content with next content', () => { mde.setMarkdown('> block\n>\nparagraph'); mde.setSelection([2, 2], [2, 2]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe('> block\n\n\nparagraph'); }); it('should not extend block quote when position is start offset', () => { mde.setMarkdown('> block'); mde.setSelection([1, 1], [1, 1]); forceKeymapFn('blockQuote', 'extendBlockQuote'); expect(getTextContent(mde)).toBe('> block'); }); it('should undo extend the block quote properly', () => { const input = '> block\nparagraph'; const result = ''; mde.setMarkdown(input); mde.setSelection([1, 6], [1, 6]); forceKeymapFn('blockQuote', 'extendBlockQuote'); execUndo(); expect(getPreviewHTML()).toBe(result); }); }); describe('extend list keymap', () => { describe('bullet list', () => { it('should extend the bullet list', () => { const input = source` * bullet `; const result = `${source` * bullet * `} `; mde.setMarkdown(input); mde.setSelection([1, 9], [1, 9]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); assertSelection([ [2, 3], [2, 3], ]); }); it('should extend the bullet list with sliced text', () => { const input = source` * bullet `; const result = source` * bull * et `; mde.setMarkdown(input); mde.setSelection([1, 7], [1, 7]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); assertSelection([ [2, 3], [2, 3], ]); }); it('should extend the nested bullet list', () => { const input = stripIndent` * bullet * sub `; const result = `${source` * bullet * sub * `} `; mde.setMarkdown(input); mde.setSelection([2, 8], [2, 8]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); }); it('should extend the bullet list excluding blank line', () => { const input = `${source` * bullet1 * bullet2 `}\n\n`; const result = `${source` * bullet1 * bullet2 * `} \n\n`; mde.setMarkdown(input); mde.setSelection([2, 10], [2, 10]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); }); it('should extend the bullet list with task', () => { const input = source` * [ ] bullet `; const result = `${source` * [ ] bullet * [ ] `} `; mde.setMarkdown(input); mde.setSelection([1, 13], [1, 13]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); assertSelection([ [2, 7], [2, 7], ]); }); it('should not extend the bullet list on multi line selection', () => { const input = source` * bullet1 * bullet2 `; mde.setMarkdown(input); mde.setSelection([1, 2], [2, 4]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(input); }); it('should delete the row in case of empty bullet list content', () => { const input = `${stripIndent` * bullet1 * `} `; const result = `${source` * bullet1 `}\n\n`; mde.setMarkdown(input); mde.setSelection([2, 2], [2, 2]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); }); it('should delete the row in case of empty bullet task list content', () => { const input = `${stripIndent` * [ ] bullet1 * [ ] `} `; const result = `${source` * [ ] bullet1 `}\n\n`; mde.setMarkdown(input); mde.setSelection([2, 5], [2, 5]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(result); }); it('should not extend list when paragraph includes `* `', () => { const input = source` just paragraph* bullet `; mde.setMarkdown(input); mde.setSelection([1, 9], [1, 9]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe(input); }); it('should delete the row in case of empty bullet list content with next content', () => { mde.setMarkdown('* bullet1\n* \nparagraph'); mde.setSelection([2, 3], [2, 3]); forceKeymapFn('listItem', 'extendList'); expect(getTextContent(mde)).toBe('* bullet1\n\n\nparagraph'); }); it('should undo extend the bullet list properly', () => { const input = source` * bullet paragraph `; const result = oneLineTrim`block
paragraph
bullet
paragraph
console.log('line1');
console.log('line2');
`;
mde.setMarkdown(input);
mde.setSelection([3, 20], [3, 20]);
forceKeymapFn('codeBlock', 'keepIndentation');
execUndo();
expect(getPreviewHTML()).toBe(result);
});
});
/* eslint-enable no-irregular-whitespace */
// @TODO: should move key event test case to e2e test
describe('default keymap', () => {
it('should delete the blank line properly when pressing the backspace key', () => {
mde.setMarkdown('# myText\n\ntest');
mde.setSelection([3, 1], [3, 1]);
forceBackspaceKeymap();
expect(getPreviewHTML()).toBe('test
'); }); }); describe('useCommandShortcut option', () => { it('should not make keymaps with history command when the value is false', () => { const spy = jest.spyOn(keymaps, 'keymap'); const useCommandShortcut = false; const history = { 'Mod-z': undo, 'Shift-Mod-z': redo, }; mde.createKeymaps(useCommandShortcut); expect(spy).not.toHaveBeenCalledWith(history); }); });