diff options
Diffstat (limited to 'spec/frontend/ide/lib')
-rw-r--r-- | spec/frontend/ide/lib/common/model_spec.js | 72 | ||||
-rw-r--r-- | spec/frontend/ide/lib/create_diff_spec.js | 182 | ||||
-rw-r--r-- | spec/frontend/ide/lib/create_file_diff_spec.js | 163 | ||||
-rw-r--r-- | spec/frontend/ide/lib/diff/diff_spec.js | 8 | ||||
-rw-r--r-- | spec/frontend/ide/lib/editor_options_spec.js | 11 | ||||
-rw-r--r-- | spec/frontend/ide/lib/editor_spec.js | 46 | ||||
-rw-r--r-- | spec/frontend/ide/lib/editorconfig/mock_data.js | 146 | ||||
-rw-r--r-- | spec/frontend/ide/lib/editorconfig/parser_spec.js | 18 | ||||
-rw-r--r-- | spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js | 43 | ||||
-rw-r--r-- | spec/frontend/ide/lib/files_spec.js | 4 | ||||
-rw-r--r-- | spec/frontend/ide/lib/mirror_spec.js | 184 |
11 files changed, 858 insertions, 19 deletions
diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js index 2ef2f0da6da..df46b7774b0 100644 --- a/spec/frontend/ide/lib/common/model_spec.js +++ b/spec/frontend/ide/lib/common/model_spec.js @@ -133,5 +133,77 @@ describe('Multi-file editor library model', () => { expect(disposeSpy).toHaveBeenCalled(); }); + + it('applies custom options and triggers onChange callback', () => { + const changeSpy = jest.fn(); + jest.spyOn(model, 'applyCustomOptions'); + + model.onChange(changeSpy); + + model.dispose(); + + expect(model.applyCustomOptions).toHaveBeenCalled(); + expect(changeSpy).toHaveBeenCalled(); + }); + }); + + describe('updateOptions', () => { + it('sets the options on the options object', () => { + model.updateOptions({ insertSpaces: true, someOption: 'some value' }); + + expect(model.options).toEqual({ + endOfLine: 0, + insertFinalNewline: true, + insertSpaces: true, + someOption: 'some value', + trimTrailingWhitespace: false, + }); + }); + + it.each` + option | value + ${'insertSpaces'} | ${true} + ${'insertSpaces'} | ${false} + ${'indentSize'} | ${4} + ${'tabSize'} | ${3} + `("correctly sets option: $option=$value to Monaco's TextModel", ({ option, value }) => { + model.updateOptions({ [option]: value }); + + expect(model.getModel().getOptions()).toMatchObject({ [option]: value }); + }); + + it('applies custom options immediately', () => { + jest.spyOn(model, 'applyCustomOptions'); + + model.updateOptions({ trimTrailingWhitespace: true, someOption: 'some value' }); + + expect(model.applyCustomOptions).toHaveBeenCalled(); + }); + }); + + describe('applyCustomOptions', () => { + it.each` + option | value | contentBefore | contentAfter + ${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'} + ${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'} + ${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'} + ${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'} + ${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'} + ${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'} + ${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'} + ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'} + ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'} + ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'} + `( + 'correctly applies custom option $option=$value to content', + ({ option, value, contentBefore, contentAfter }) => { + model.options[option] = value; + + model.updateNewContent(contentBefore); + model.applyCustomOptions(); + + expect(model.getModel().getValue()).toEqual(contentAfter); + }, + ); }); }); diff --git a/spec/frontend/ide/lib/create_diff_spec.js b/spec/frontend/ide/lib/create_diff_spec.js new file mode 100644 index 00000000000..273f9ee27bd --- /dev/null +++ b/spec/frontend/ide/lib/create_diff_spec.js @@ -0,0 +1,182 @@ +import createDiff from '~/ide/lib/create_diff'; +import createFileDiff from '~/ide/lib/create_file_diff'; +import { commitActionTypes } from '~/ide/constants'; +import { + createNewFile, + createUpdatedFile, + createDeletedFile, + createMovedFile, + createEntries, +} from '../file_helpers'; + +const PATH_FOO = 'test/foo.md'; +const PATH_BAR = 'test/bar.md'; +const PATH_ZED = 'test/zed.md'; +const PATH_LOREM = 'test/lipsum/nested/lorem.md'; +const PATH_IPSUM = 'test/lipsum/ipsum.md'; +const TEXT = `Lorem ipsum dolor sit amet, +consectetur adipiscing elit. +Morbi ex dolor, euismod nec rutrum nec, egestas at ligula. +Praesent scelerisque ut nisi eu eleifend. +Suspendisse potenti. +`; +const LINES = TEXT.trim().split('\n'); + +const joinDiffs = (...patches) => patches.join(''); + +describe('IDE lib/create_diff', () => { + it('with created files, generates patch', () => { + const changedFiles = [createNewFile(PATH_FOO, TEXT), createNewFile(PATH_BAR, '')]; + const result = createDiff({ changedFiles }); + + expect(result).toEqual({ + patch: joinDiffs( + createFileDiff(changedFiles[0], commitActionTypes.create), + createFileDiff(changedFiles[1], commitActionTypes.create), + ), + toDelete: [], + }); + }); + + it('with deleted files, adds to delete', () => { + const changedFiles = [createDeletedFile(PATH_FOO, TEXT), createDeletedFile(PATH_BAR, '')]; + + const result = createDiff({ changedFiles }); + + expect(result).toEqual({ + patch: '', + toDelete: [PATH_FOO, PATH_BAR], + }); + }); + + it('with updated files, generates patch', () => { + const changedFiles = [createUpdatedFile(PATH_FOO, TEXT, 'A change approaches!')]; + + const result = createDiff({ changedFiles }); + + expect(result).toEqual({ + patch: createFileDiff(changedFiles[0], commitActionTypes.update), + toDelete: [], + }); + }); + + it('with files in both staged and changed, prefer changed', () => { + const changedFiles = [ + createUpdatedFile(PATH_FOO, TEXT, 'Do a change!'), + createDeletedFile(PATH_LOREM), + ]; + + const result = createDiff({ + changedFiles, + stagedFiles: [createUpdatedFile(PATH_LOREM, TEXT, ''), createDeletedFile(PATH_FOO, TEXT)], + }); + + expect(result).toEqual({ + patch: createFileDiff(changedFiles[0], commitActionTypes.update), + toDelete: [PATH_LOREM], + }); + }); + + it('with file created in staging and deleted in changed, do nothing', () => { + const result = createDiff({ + changedFiles: [createDeletedFile(PATH_FOO)], + stagedFiles: [createNewFile(PATH_FOO, TEXT)], + }); + + expect(result).toEqual({ + patch: '', + toDelete: [], + }); + }); + + it('with file deleted in both staged and changed, delete', () => { + const result = createDiff({ + changedFiles: [createDeletedFile(PATH_LOREM)], + stagedFiles: [createDeletedFile(PATH_LOREM)], + }); + + expect(result).toEqual({ + patch: '', + toDelete: [PATH_LOREM], + }); + }); + + it('with file moved, create and delete', () => { + const changedFiles = [createMovedFile(PATH_BAR, PATH_FOO, TEXT)]; + + const result = createDiff({ + changedFiles, + stagedFiles: [createDeletedFile(PATH_FOO)], + }); + + expect(result).toEqual({ + patch: createFileDiff(changedFiles[0], commitActionTypes.create), + toDelete: [PATH_FOO], + }); + }); + + it('with file moved and no content, move', () => { + const changedFiles = [createMovedFile(PATH_BAR, PATH_FOO)]; + + const result = createDiff({ + changedFiles, + stagedFiles: [createDeletedFile(PATH_FOO)], + }); + + expect(result).toEqual({ + patch: createFileDiff(changedFiles[0], commitActionTypes.move), + toDelete: [], + }); + }); + + it('creates a well formatted patch', () => { + const changedFiles = [ + createMovedFile(PATH_BAR, PATH_FOO), + createDeletedFile(PATH_ZED), + createNewFile(PATH_LOREM, TEXT), + createUpdatedFile(PATH_IPSUM, TEXT, "That's all folks!"), + ]; + + const expectedPatch = `diff --git "a/${PATH_FOO}" "b/${PATH_BAR}" +rename from ${PATH_FOO} +rename to ${PATH_BAR} +diff --git "a/${PATH_LOREM}" "b/${PATH_LOREM}" +new file mode 100644 +--- /dev/null ++++ b/${PATH_LOREM} +@@ -0,0 +1,${LINES.length} @@ +${LINES.map(line => `+${line}`).join('\n')} +diff --git "a/${PATH_IPSUM}" "b/${PATH_IPSUM}" +--- a/${PATH_IPSUM} ++++ b/${PATH_IPSUM} +@@ -1,${LINES.length} +1,1 @@ +${LINES.map(line => `-${line}`).join('\n')} ++That's all folks! +\\ No newline at end of file +`; + + const result = createDiff({ changedFiles }); + + expect(result).toEqual({ + patch: expectedPatch, + toDelete: [PATH_ZED], + }); + }); + + it('deletes deleted parent directories', () => { + const deletedFiles = ['foo/bar/zed/test.md', 'foo/bar/zed/test2.md']; + const entries = deletedFiles.reduce((acc, path) => Object.assign(acc, createEntries(path)), {}); + const allDeleted = [...deletedFiles, 'foo/bar/zed', 'foo/bar']; + allDeleted.forEach(path => { + entries[path].deleted = true; + }); + const changedFiles = deletedFiles.map(x => entries[x]); + + const result = createDiff({ changedFiles, entries }); + + expect(result).toEqual({ + patch: '', + toDelete: allDeleted, + }); + }); +}); diff --git a/spec/frontend/ide/lib/create_file_diff_spec.js b/spec/frontend/ide/lib/create_file_diff_spec.js new file mode 100644 index 00000000000..4b428468a6d --- /dev/null +++ b/spec/frontend/ide/lib/create_file_diff_spec.js @@ -0,0 +1,163 @@ +import createFileDiff from '~/ide/lib/create_file_diff'; +import { commitActionTypes } from '~/ide/constants'; +import { + createUpdatedFile, + createNewFile, + createMovedFile, + createDeletedFile, +} from '../file_helpers'; + +const PATH = 'test/numbers.md'; +const PATH_FOO = 'test/foo.md'; +const TEXT_LINE_COUNT = 100; +const TEXT = Array(TEXT_LINE_COUNT) + .fill(0) + .map((_, idx) => `${idx + 1}`) + .join('\n'); + +const spliceLines = (content, lineNumber, deleteCount = 0, newLines = []) => { + const lines = content.split('\n'); + lines.splice(lineNumber, deleteCount, ...newLines); + return lines.join('\n'); +}; + +const mapLines = (content, mapFn) => + content + .split('\n') + .map(mapFn) + .join('\n'); + +describe('IDE lib/create_file_diff', () => { + it('returns empty string with "garbage" action', () => { + const result = createFileDiff(createNewFile(PATH, ''), 'garbage'); + + expect(result).toBe(''); + }); + + it('preserves ending whitespace in file', () => { + const oldContent = spliceLines(TEXT, 99, 1, ['100 ']); + const newContent = spliceLines(oldContent, 99, 0, ['Lorem', 'Ipsum']); + const expected = ` + 99 ++Lorem ++Ipsum + 100 `; + + const result = createFileDiff( + createUpdatedFile(PATH, oldContent, newContent), + commitActionTypes.update, + ); + + expect(result).toContain(expected); + }); + + describe('with "create" action', () => { + const expectedHead = `diff --git "a/${PATH}" "b/${PATH}" +new file mode 100644`; + + const expectedChunkHead = lineCount => `--- /dev/null ++++ b/${PATH} +@@ -0,0 +1,${lineCount} @@`; + + it('with empty file, does not include diff body', () => { + const result = createFileDiff(createNewFile(PATH, ''), commitActionTypes.create); + + expect(result).toBe(`${expectedHead}\n`); + }); + + it('with single line, includes diff body', () => { + const result = createFileDiff(createNewFile(PATH, '\n'), commitActionTypes.create); + + expect(result).toBe(`${expectedHead} +${expectedChunkHead(1)} ++ +`); + }); + + it('without newline, includes no newline comment', () => { + const result = createFileDiff(createNewFile(PATH, 'Lorem ipsum'), commitActionTypes.create); + + expect(result).toBe(`${expectedHead} +${expectedChunkHead(1)} ++Lorem ipsum +\\ No newline at end of file +`); + }); + + it('with content, includes diff body', () => { + const content = `${TEXT}\n`; + const result = createFileDiff(createNewFile(PATH, content), commitActionTypes.create); + + expect(result).toBe(`${expectedHead} +${expectedChunkHead(TEXT_LINE_COUNT)} +${mapLines(TEXT, line => `+${line}`)} +`); + }); + }); + + describe('with "delete" action', () => { + const expectedHead = `diff --git "a/${PATH}" "b/${PATH}" +deleted file mode 100644`; + + const expectedChunkHead = lineCount => `--- a/${PATH} ++++ /dev/null +@@ -1,${lineCount} +0,0 @@`; + + it('with empty file, does not include diff body', () => { + const result = createFileDiff(createDeletedFile(PATH, ''), commitActionTypes.delete); + + expect(result).toBe(`${expectedHead}\n`); + }); + + it('with content, includes diff body', () => { + const content = `${TEXT}\n`; + const result = createFileDiff(createDeletedFile(PATH, content), commitActionTypes.delete); + + expect(result).toBe(`${expectedHead} +${expectedChunkHead(TEXT_LINE_COUNT)} +${mapLines(TEXT, line => `-${line}`)} +`); + }); + }); + + describe('with "update" action', () => { + it('includes diff body', () => { + const oldContent = `${TEXT}\n`; + const newContent = `${spliceLines(TEXT, 50, 3, ['Lorem'])}\n`; + + const result = createFileDiff( + createUpdatedFile(PATH, oldContent, newContent), + commitActionTypes.update, + ); + + expect(result).toBe(`diff --git "a/${PATH}" "b/${PATH}" +--- a/${PATH} ++++ b/${PATH} +@@ -47,11 +47,9 @@ + 47 + 48 + 49 + 50 +-51 +-52 +-53 ++Lorem + 54 + 55 + 56 + 57 +`); + }); + }); + + describe('with "move" action', () => { + it('returns rename head', () => { + const result = createFileDiff(createMovedFile(PATH, PATH_FOO), commitActionTypes.move); + + expect(result).toBe(`diff --git "a/${PATH_FOO}" "b/${PATH}" +rename from ${PATH_FOO} +rename to ${PATH} +`); + }); + }); +}); diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js index d9b088e2c12..901f9e7cfd1 100644 --- a/spec/frontend/ide/lib/diff/diff_spec.js +++ b/spec/frontend/ide/lib/diff/diff_spec.js @@ -73,5 +73,13 @@ describe('Multi-file editor library diff calculator', () => { expect(diff.endLineNumber).toBe(1); }); + + it('disregards changes for EOL type changes', () => { + const text1 = 'line1\nline2\nline3\n'; + const text2 = 'line1\r\nline2\r\nline3\r\n'; + + expect(computeDiff(text1, text2)).toEqual([]); + expect(computeDiff(text2, text1)).toEqual([]); + }); }); }); diff --git a/spec/frontend/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js deleted file mode 100644 index b07a583b7c8..00000000000 --- a/spec/frontend/ide/lib/editor_options_spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import editorOptions from '~/ide/lib/editor_options'; - -describe('Multi-file editor library editor options', () => { - it('returns an array', () => { - expect(editorOptions).toEqual(expect.any(Array)); - }); - - it('contains readOnly option', () => { - expect(editorOptions[0].readOnly).toBeDefined(); - }); -}); diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js index 36d4c3c26ee..f5815771cdf 100644 --- a/spec/frontend/ide/lib/editor_spec.js +++ b/spec/frontend/ide/lib/editor_spec.js @@ -1,4 +1,9 @@ -import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor'; +import { + editor as monacoEditor, + languages as monacoLanguages, + Range, + Selection, +} from 'monaco-editor'; import Editor from '~/ide/lib/editor'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import { file } from '../helpers'; @@ -72,12 +77,13 @@ describe('Multi-file editor library', () => { expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, { ...defaultEditorOptions, + ignoreTrimWhitespace: false, quickSuggestions: false, occurrencesHighlight: false, renderSideBySide: false, - readOnly: true, - renderLineHighlight: 'all', - hideCursorInOverviewRuler: false, + readOnly: false, + renderLineHighlight: 'none', + hideCursorInOverviewRuler: true, }); }); }); @@ -193,6 +199,38 @@ describe('Multi-file editor library', () => { }); }); + describe('replaceSelectedText', () => { + let model; + let editor; + + beforeEach(() => { + instance.createInstance(holder); + + model = instance.createModel({ + ...file(), + key: 'index.md', + path: 'index.md', + }); + + instance.attachModel(model); + + editor = instance.instance; + editor.getModel().setValue('foo bar baz'); + editor.setSelection(new Range(1, 5, 1, 8)); + + instance.replaceSelectedText('hello'); + }); + + it('replaces the text selected in editor with the one provided', () => { + expect(editor.getModel().getValue()).toBe('foo hello baz'); + }); + + it('sets cursor to end of the replaced string', () => { + const selection = editor.getSelection(); + expect(selection).toEqual(new Selection(1, 10, 1, 10)); + }); + }); + describe('dispose', () => { it('calls disposble dispose method', () => { jest.spyOn(instance.disposable, 'dispose'); diff --git a/spec/frontend/ide/lib/editorconfig/mock_data.js b/spec/frontend/ide/lib/editorconfig/mock_data.js new file mode 100644 index 00000000000..b21f4a5b735 --- /dev/null +++ b/spec/frontend/ide/lib/editorconfig/mock_data.js @@ -0,0 +1,146 @@ +export const exampleConfigs = [ + { + path: 'foo/bar/baz/.editorconfig', + content: ` +[*] +tab_width = 6 +indent_style = tab +`, + }, + { + path: 'foo/bar/.editorconfig', + content: ` +root = false + +[*] +indent_size = 5 +indent_style = space +trim_trailing_whitespace = true + +[*_spec.{js,py}] +end_of_line = crlf + `, + }, + { + path: 'foo/.editorconfig', + content: ` +[*] +tab_width = 4 +indent_style = tab + `, + }, + { + path: '.editorconfig', + content: ` +root = true + +[*] +indent_size = 3 +indent_style = space +end_of_line = lf +insert_final_newline = true + +[*.js] +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[*.txt] +end_of_line = crlf + `, + }, + { + path: 'foo/bar/root/.editorconfig', + content: ` +root = true + +[*] +tab_width = 1 +indent_style = tab + `, + }, +]; + +export const exampleFiles = [ + { + path: 'foo/bar/root/README.md', + rules: { + indent_style: 'tab', // foo/bar/root/.editorconfig + tab_width: '1', // foo/bar/root/.editorconfig + }, + monacoRules: { + insertSpaces: false, + tabSize: 1, + }, + }, + { + path: 'foo/bar/baz/my_spec.js', + rules: { + end_of_line: 'crlf', // foo/bar/.editorconfig (for _spec.js files) + indent_size: '5', // foo/bar/.editorconfig + indent_style: 'tab', // foo/bar/baz/.editorconfig + insert_final_newline: 'true', // .editorconfig + tab_width: '6', // foo/bar/baz/.editorconfig + trim_trailing_whitespace: 'true', // .editorconfig (for .js files) + }, + monacoRules: { + endOfLine: 1, + insertFinalNewline: true, + insertSpaces: false, + tabSize: 6, + trimTrailingWhitespace: true, + }, + }, + { + path: 'foo/my_file.js', + rules: { + end_of_line: 'lf', // .editorconfig + indent_size: '2', // .editorconfig (for .js files) + indent_style: 'tab', // foo/.editorconfig + insert_final_newline: 'true', // .editorconfig + tab_width: '4', // foo/.editorconfig + trim_trailing_whitespace: 'true', // .editorconfig (for .js files) + }, + monacoRules: { + endOfLine: 0, + insertFinalNewline: true, + insertSpaces: false, + tabSize: 4, + trimTrailingWhitespace: true, + }, + }, + { + path: 'foo/my_file.md', + rules: { + end_of_line: 'lf', // .editorconfig + indent_size: '3', // .editorconfig + indent_style: 'tab', // foo/.editorconfig + insert_final_newline: 'true', // .editorconfig + tab_width: '4', // foo/.editorconfig + }, + monacoRules: { + endOfLine: 0, + insertFinalNewline: true, + insertSpaces: false, + tabSize: 4, + }, + }, + { + path: 'foo/bar/my_file.txt', + rules: { + end_of_line: 'crlf', // .editorconfig (for .txt files) + indent_size: '5', // foo/bar/.editorconfig + indent_style: 'space', // foo/bar/.editorconfig + insert_final_newline: 'true', // .editorconfig + tab_width: '4', // foo/.editorconfig + trim_trailing_whitespace: 'true', // foo/bar/.editorconfig + }, + monacoRules: { + endOfLine: 1, + insertFinalNewline: true, + insertSpaces: true, + tabSize: 4, + trimTrailingWhitespace: true, + }, + }, +]; diff --git a/spec/frontend/ide/lib/editorconfig/parser_spec.js b/spec/frontend/ide/lib/editorconfig/parser_spec.js new file mode 100644 index 00000000000..f99410236e1 --- /dev/null +++ b/spec/frontend/ide/lib/editorconfig/parser_spec.js @@ -0,0 +1,18 @@ +import { getRulesWithTraversal } from '~/ide/lib/editorconfig/parser'; +import { exampleConfigs, exampleFiles } from './mock_data'; + +describe('~/ide/lib/editorconfig/parser', () => { + const getExampleConfigContent = path => + Promise.resolve(exampleConfigs.find(x => x.path === path)?.content); + + describe('getRulesWithTraversal', () => { + it.each(exampleFiles)( + 'traverses through all editorconfig files in parent directories (until root=true is hit) and finds rules for this file (case %#)', + ({ path, rules }) => { + return getRulesWithTraversal(path, getExampleConfigContent).then(result => { + expect(result).toEqual(rules); + }); + }, + ); + }); +}); diff --git a/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js b/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js new file mode 100644 index 00000000000..536b1409435 --- /dev/null +++ b/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js @@ -0,0 +1,43 @@ +import mapRulesToMonaco from '~/ide/lib/editorconfig/rules_mapper'; + +describe('mapRulesToMonaco', () => { + const multipleEntries = { + input: { indent_style: 'tab', indent_size: '4', insert_final_newline: 'true' }, + output: { insertSpaces: false, tabSize: 4, insertFinalNewline: true }, + }; + + // tab width takes precedence + const tabWidthAndIndent = { + input: { indent_style: 'tab', indent_size: '4', tab_width: '3' }, + output: { insertSpaces: false, tabSize: 3 }, + }; + + it.each` + rule | monacoOption + ${{ indent_style: 'tab' }} | ${{ insertSpaces: false }} + ${{ indent_style: 'space' }} | ${{ insertSpaces: true }} + ${{ indent_style: 'unset' }} | ${{}} + ${{ indent_size: '4' }} | ${{ tabSize: 4 }} + ${{ indent_size: '4.4' }} | ${{ tabSize: 4 }} + ${{ indent_size: '0' }} | ${{}} + ${{ indent_size: '-10' }} | ${{}} + ${{ indent_size: 'NaN' }} | ${{}} + ${{ tab_width: '4' }} | ${{ tabSize: 4 }} + ${{ tab_width: '5.4' }} | ${{ tabSize: 5 }} + ${{ tab_width: '-10' }} | ${{}} + ${{ trim_trailing_whitespace: 'true' }} | ${{ trimTrailingWhitespace: true }} + ${{ trim_trailing_whitespace: 'false' }} | ${{ trimTrailingWhitespace: false }} + ${{ trim_trailing_whitespace: 'unset' }} | ${{}} + ${{ end_of_line: 'lf' }} | ${{ endOfLine: 0 }} + ${{ end_of_line: 'crlf' }} | ${{ endOfLine: 1 }} + ${{ end_of_line: 'cr' }} | ${{}} + ${{ end_of_line: 'unset' }} | ${{}} + ${{ insert_final_newline: 'true' }} | ${{ insertFinalNewline: true }} + ${{ insert_final_newline: 'false' }} | ${{ insertFinalNewline: false }} + ${{ insert_final_newline: 'unset' }} | ${{}} + ${multipleEntries.input} | ${multipleEntries.output} + ${tabWidthAndIndent.input} | ${tabWidthAndIndent.output} + `('correctly maps editorconfig rule to monaco option: $rule', ({ rule, monacoOption }) => { + expect(mapRulesToMonaco(rule)).toEqual(monacoOption); + }); +}); diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index 2b15aef6454..6974cdc4074 100644 --- a/spec/frontend/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js @@ -11,7 +11,6 @@ const createEntries = paths => { const createUrl = base => (type === 'tree' ? `${base}/` : base); const { name, parent } = splitParent(path); - const parentEntry = acc[parent]; const previewMode = viewerInformationForPath(name); acc[path] = { @@ -26,9 +25,6 @@ const createEntries = paths => { previewMode, binary: (previewMode && previewMode.binary) || false, parentPath: parent, - parentTreeUrl: parentEntry - ? parentEntry.url - : createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}`), }), tree: children.map(childName => expect.objectContaining({ name: childName })), }; diff --git a/spec/frontend/ide/lib/mirror_spec.js b/spec/frontend/ide/lib/mirror_spec.js new file mode 100644 index 00000000000..21bed5948f3 --- /dev/null +++ b/spec/frontend/ide/lib/mirror_spec.js @@ -0,0 +1,184 @@ +import createDiff from '~/ide/lib/create_diff'; +import { + canConnect, + createMirror, + SERVICE_NAME, + PROTOCOL, + MSG_CONNECTION_ERROR, + SERVICE_DELAY, +} from '~/ide/lib/mirror'; +import { getWebSocketUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/ide/lib/create_diff', () => jest.fn()); + +const TEST_PATH = '/project/ide/proxy/path'; +const TEST_DIFF = { + patch: 'lorem ipsum', + toDelete: ['foo.md'], +}; +const TEST_ERROR = 'Something bad happened...'; +const TEST_SUCCESS_RESPONSE = { + data: JSON.stringify({ error: { code: 0 }, payload: { status_code: 200 } }), +}; +const TEST_ERROR_RESPONSE = { + data: JSON.stringify({ error: { code: 1, Message: TEST_ERROR }, payload: { status_code: 200 } }), +}; +const TEST_ERROR_PAYLOAD_RESPONSE = { + data: JSON.stringify({ + error: { code: 0 }, + payload: { status_code: 500, error_message: TEST_ERROR }, + }), +}; + +const buildUploadMessage = ({ toDelete, patch }) => + JSON.stringify({ + code: 'EVENT', + namespace: '/files', + event: 'PATCH', + payload: { diff: patch, delete_files: toDelete }, + }); + +describe('ide/lib/mirror', () => { + describe('canConnect', () => { + it('can connect if the session has the expected service', () => { + const result = canConnect({ services: ['test1', SERVICE_NAME, 'test2'] }); + + expect(result).toBe(true); + }); + + it('cannot connect if the session does not have the expected service', () => { + const result = canConnect({ services: ['test1', 'test2'] }); + + expect(result).toBe(false); + }); + }); + + describe('createMirror', () => { + const origWebSocket = global.WebSocket; + let mirror; + let mockWebSocket; + + beforeEach(() => { + mockWebSocket = { + close: jest.fn(), + send: jest.fn(), + }; + global.WebSocket = jest.fn().mockImplementation(() => mockWebSocket); + mirror = createMirror(); + }); + + afterEach(() => { + global.WebSocket = origWebSocket; + }); + + const waitForConnection = (delay = SERVICE_DELAY) => { + const wait = new Promise(resolve => { + setTimeout(resolve, 10); + }); + + jest.advanceTimersByTime(delay); + + return wait; + }; + const connectPass = () => waitForConnection().then(() => mockWebSocket.onopen()); + const connectFail = () => waitForConnection().then(() => mockWebSocket.onerror()); + const sendResponse = msg => { + mockWebSocket.onmessage(msg); + }; + + describe('connect', () => { + let connection; + + beforeEach(() => { + connection = mirror.connect(TEST_PATH); + }); + + it('waits before creating web socket', () => { + // ignore error when test suite terminates + connection.catch(() => {}); + + return waitForConnection(SERVICE_DELAY - 10).then(() => { + expect(global.WebSocket).not.toHaveBeenCalled(); + }); + }); + + it('is canceled when disconnected before finished waiting', () => { + mirror.disconnect(); + + return waitForConnection(SERVICE_DELAY).then(() => { + expect(global.WebSocket).not.toHaveBeenCalled(); + }); + }); + + describe('when connection is successful', () => { + beforeEach(connectPass); + + it('connects to service', () => { + const expectedPath = `${getWebSocketUrl(TEST_PATH)}?service=${SERVICE_NAME}`; + + return connection.then(() => { + expect(global.WebSocket).toHaveBeenCalledWith(expectedPath, [PROTOCOL]); + }); + }); + + it('disconnects when connected again', () => { + const result = connection + .then(() => { + // https://gitlab.com/gitlab-org/gitlab/issues/33024 + // eslint-disable-next-line promise/no-nesting + mirror.connect(TEST_PATH).catch(() => {}); + }) + .then(() => { + expect(mockWebSocket.close).toHaveBeenCalled(); + }); + + return result; + }); + }); + + describe('when connection fails', () => { + beforeEach(connectFail); + + it('rejects with error', () => { + return expect(connection).rejects.toEqual(new Error(MSG_CONNECTION_ERROR)); + }); + }); + }); + + describe('upload', () => { + let state; + + beforeEach(() => { + state = { changedFiles: [] }; + createDiff.mockReturnValue(TEST_DIFF); + + const connection = mirror.connect(TEST_PATH); + + return connectPass().then(() => connection); + }); + + it('creates a diff from the given state', () => { + const result = mirror.upload(state); + + sendResponse(TEST_SUCCESS_RESPONSE); + + return result.then(() => { + expect(createDiff).toHaveBeenCalledWith(state); + expect(mockWebSocket.send).toHaveBeenCalledWith(buildUploadMessage(TEST_DIFF)); + }); + }); + + it.each` + response | description + ${TEST_ERROR_RESPONSE} | ${'error in error'} + ${TEST_ERROR_PAYLOAD_RESPONSE} | ${'error in payload'} + `('rejects if response has $description', ({ response }) => { + const result = mirror.upload(state); + + sendResponse(response); + + return expect(result).rejects.toEqual({ message: TEST_ERROR }); + }); + }); + }); +}); |