From a09983ae35713f5a2bbb100981116d31ce99826e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Jul 2020 12:26:25 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-2-stable-ee --- spec/frontend/editor/editor_lite_spec.js | 70 ++++++++ spec/frontend/editor/editor_markdown_ext_spec.js | 204 +++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 spec/frontend/editor/editor_markdown_ext_spec.js (limited to 'spec/frontend/editor') diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js index cb07bcf8f28..92a136835bf 100644 --- a/spec/frontend/editor/editor_lite_spec.js +++ b/spec/frontend/editor/editor_lite_spec.js @@ -115,6 +115,76 @@ describe('Base editor', () => { }); }); + describe('extensions', () => { + const foo1 = jest.fn(); + const foo2 = jest.fn(); + const bar = jest.fn(); + const MyExt1 = { + foo: foo1, + }; + const MyExt2 = { + bar, + }; + const MyExt3 = { + foo: foo2, + }; + beforeEach(() => { + editor.createInstance({ el: editorEl, blobPath, blobContent }); + }); + + afterEach(() => { + editor.model.dispose(); + }); + + it('is extensible with the extensions', () => { + expect(editor.foo).toBeUndefined(); + + editor.use(MyExt1); + expect(editor.foo).toEqual(foo1); + }); + + it('does not fail if no extensions supplied', () => { + const spy = jest.spyOn(global.console, 'error'); + editor.use(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('is extensible with multiple extensions', () => { + expect(editor.foo).toBeUndefined(); + expect(editor.bar).toBeUndefined(); + + editor.use([MyExt1, MyExt2]); + + expect(editor.foo).toEqual(foo1); + expect(editor.bar).toEqual(bar); + }); + + it('uses the last definition of a method in case of an overlap', () => { + editor.use([MyExt1, MyExt2, MyExt3]); + expect(editor).toEqual( + expect.objectContaining({ + foo: foo2, + bar, + }), + ); + }); + + it('correctly resolves references withing extensions', () => { + const FunctionExt = { + inst() { + return this.instance; + }, + mod() { + return this.model; + }, + }; + editor.use(FunctionExt); + expect(editor.inst()).toEqual(editor.instance); + expect(editor.mod()).toEqual(editor.model); + }); + }); + describe('languages', () => { it('registers custom languages defined with Monaco', () => { expect(monacoLanguages.getLanguages()).toEqual( diff --git a/spec/frontend/editor/editor_markdown_ext_spec.js b/spec/frontend/editor/editor_markdown_ext_spec.js new file mode 100644 index 00000000000..aad2400c0f0 --- /dev/null +++ b/spec/frontend/editor/editor_markdown_ext_spec.js @@ -0,0 +1,204 @@ +import EditorLite from '~/editor/editor_lite'; +import { Range, Position } from 'monaco-editor'; +import EditorMarkdownExtension from '~/editor/editor_markdown_ext'; + +describe('Markdown Extension for Editor Lite', () => { + let editor; + let editorEl; + const firstLine = 'This is a'; + const secondLine = 'multiline'; + const thirdLine = 'string with some **markup**'; + const text = `${firstLine}\n${secondLine}\n${thirdLine}`; + const filePath = 'foo.md'; + + const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => { + const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn); + editor.instance.setSelection(selection); + }; + const selectSecondString = () => setSelection(2, 1, 2, secondLine.length + 1); // select the whole second line + const selectSecondAndThirdLines = () => setSelection(2, 1, 3, thirdLine.length + 1); // select second and third lines + + const selectionToString = () => editor.instance.getSelection().toString(); + const positionToString = () => editor.instance.getPosition().toString(); + + beforeEach(() => { + setFixtures('
'); + editorEl = document.getElementById('editor'); + editor = new EditorLite(); + editor.createInstance({ + el: editorEl, + blobPath: filePath, + blobContent: text, + }); + editor.use(EditorMarkdownExtension); + }); + + afterEach(() => { + editor.instance.dispose(); + editor.model.dispose(); + editorEl.remove(); + }); + + describe('getSelectedText', () => { + it('does not fail if there is no selection and returns the empty string', () => { + jest.spyOn(editor.instance, 'getSelection'); + const resText = editor.getSelectedText(); + + expect(editor.instance.getSelection).toHaveBeenCalled(); + expect(resText).toBe(''); + }); + + it.each` + description | selection | expectedString + ${'same-line'} | ${[1, 1, 1, firstLine.length + 1]} | ${firstLine} + ${'two-lines'} | ${[1, 1, 2, secondLine.length + 1]} | ${`${firstLine}\n${secondLine}`} + ${'multi-lines'} | ${[1, 1, 3, thirdLine.length + 1]} | ${text} + `('correctly returns selected text for $description', ({ selection, expectedString }) => { + setSelection(...selection); + + const resText = editor.getSelectedText(); + + expect(resText).toBe(expectedString); + }); + + it('accepts selection object that serves as a source instead of current selection', () => { + selectSecondString(); + const firstLineSelection = new Range(1, 1, 1, firstLine.length + 1); + + const resText = editor.getSelectedText(firstLineSelection); + + expect(resText).toBe(firstLine); + }); + }); + + describe('replaceSelectedText', () => { + const expectedStr = 'foo'; + + it('replaces selected text with the supplied one', () => { + selectSecondString(); + editor.replaceSelectedText(expectedStr); + + expect(editor.getValue()).toBe(`${firstLine}\n${expectedStr}\n${thirdLine}`); + }); + + it('prepends the supplied text if no text is selected', () => { + editor.replaceSelectedText(expectedStr); + expect(editor.getValue()).toBe(`${expectedStr}${firstLine}\n${secondLine}\n${thirdLine}`); + }); + + it('replaces selection with empty string if no text is supplied', () => { + selectSecondString(); + editor.replaceSelectedText(); + expect(editor.getValue()).toBe(`${firstLine}\n\n${thirdLine}`); + }); + + it('puts cursor at the end of the new string and collapses selection by default', () => { + selectSecondString(); + editor.replaceSelectedText(expectedStr); + + expect(positionToString()).toBe(`(2,${expectedStr.length + 1})`); + expect(selectionToString()).toBe( + `[2,${expectedStr.length + 1} -> 2,${expectedStr.length + 1}]`, + ); + }); + + it('puts cursor at the end of the new string and keeps selection if "select" is supplied', () => { + const select = 'url'; + const complexReplacementString = `[${secondLine}](${select})`; + selectSecondString(); + editor.replaceSelectedText(complexReplacementString, select); + + expect(positionToString()).toBe(`(2,${complexReplacementString.length + 1})`); + expect(selectionToString()).toBe(`[2,1 -> 2,${complexReplacementString.length + 1}]`); + }); + }); + + describe('moveCursor', () => { + const setPosition = endCol => { + const currentPos = new Position(2, endCol); + editor.instance.setPosition(currentPos); + }; + + it.each` + direction | condition | startColumn | shift | endPosition + ${'left'} | ${'negative'} | ${secondLine.length + 1} | ${-1} | ${`(2,${secondLine.length})`} + ${'left'} | ${'negative'} | ${secondLine.length} | ${secondLine.length * -1} | ${'(2,1)'} + ${'right'} | ${'positive'} | ${1} | ${1} | ${'(2,2)'} + ${'right'} | ${'positive'} | ${2} | ${secondLine.length} | ${`(2,${secondLine.length + 1})`} + ${'up'} | ${'positive'} | ${1} | ${[0, -1]} | ${'(1,1)'} + ${'top of file'} | ${'positive'} | ${1} | ${[0, -100]} | ${'(1,1)'} + ${'down'} | ${'negative'} | ${1} | ${[0, 1]} | ${'(3,1)'} + ${'end of file'} | ${'negative'} | ${1} | ${[0, 100]} | ${`(3,${thirdLine.length + 1})`} + ${'end of line'} | ${'too large'} | ${1} | ${secondLine.length + 100} | ${`(2,${secondLine.length + 1})`} + ${'start of line'} | ${'too low'} | ${1} | ${-100} | ${'(2,1)'} + `( + 'moves cursor to the $direction if $condition supplied', + ({ startColumn, shift, endPosition }) => { + setPosition(startColumn); + if (Array.isArray(shift)) { + editor.moveCursor(...shift); + } else { + editor.moveCursor(shift); + } + expect(positionToString()).toBe(endPosition); + }, + ); + }); + + describe('selectWithinSelection', () => { + it('scopes down current selection to supplied text', () => { + const selectedText = `${secondLine}\n${thirdLine}`; + const toSelect = 'string'; + selectSecondAndThirdLines(); + + expect(selectionToString()).toBe(`[2,1 -> 3,${thirdLine.length + 1}]`); + + editor.selectWithinSelection(toSelect, selectedText); + expect(selectionToString()).toBe(`[3,1 -> 3,${toSelect.length + 1}]`); + }); + + it('does not fail when only `toSelect` is supplied and fetches the text from selection', () => { + jest.spyOn(editor, 'getSelectedText'); + const toSelect = 'string'; + selectSecondAndThirdLines(); + + editor.selectWithinSelection(toSelect); + + expect(editor.getSelectedText).toHaveBeenCalled(); + expect(selectionToString()).toBe(`[3,1 -> 3,${toSelect.length + 1}]`); + }); + + it('does nothing if no `toSelect` is supplied', () => { + selectSecondAndThirdLines(); + const expectedPos = `(3,${thirdLine.length + 1})`; + const expectedSelection = `[2,1 -> 3,${thirdLine.length + 1}]`; + + expect(positionToString()).toBe(expectedPos); + expect(selectionToString()).toBe(expectedSelection); + + editor.selectWithinSelection(); + + expect(positionToString()).toBe(expectedPos); + expect(selectionToString()).toBe(expectedSelection); + }); + + it('does nothing if no selection is set in the editor', () => { + const expectedPos = '(1,1)'; + const expectedSelection = '[1,1 -> 1,1]'; + const toSelect = 'string'; + + expect(positionToString()).toBe(expectedPos); + expect(selectionToString()).toBe(expectedSelection); + + editor.selectWithinSelection(toSelect); + + expect(positionToString()).toBe(expectedPos); + expect(selectionToString()).toBe(expectedSelection); + + editor.selectWithinSelection(); + + expect(positionToString()).toBe(expectedPos); + expect(selectionToString()).toBe(expectedSelection); + }); + }); +}); -- cgit v1.2.1