diff --git a/client/modules/IDE/components/Editor/stateUtils.test.js b/client/modules/IDE/components/Editor/stateUtils.test.js new file mode 100644 index 0000000000..c8dd38dfef --- /dev/null +++ b/client/modules/IDE/components/Editor/stateUtils.test.js @@ -0,0 +1,539 @@ +import { EditorState } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; +import { keymap } from '@codemirror/view'; +import * as tidier from './tidier'; +import { getFileMode, createNewFileState } from './stateUtils'; + +describe('getFileMode', () => { + it('Returns correct javascript file mode', () => { + const fileName = 'file1.js'; + const mode = getFileMode(fileName); + const expectedMode = 'javascript'; + + expect(mode).toBe(expectedMode); + }); + + it('Returns correct css file mode', () => { + const fileName = 'file1.css'; + const mode = getFileMode(fileName); + const expectedMode = 'css'; + + expect(mode).toBe(expectedMode); + }); + + it('Returns correct html file mode', () => { + const fileName = 'file1.html'; + const mode = getFileMode(fileName); + const expectedMode = 'html'; + + expect(mode).toBe(expectedMode); + }); + + it('Returns correct xml file mode', () => { + const fileName = 'file1.xml'; + const mode = getFileMode(fileName); + const expectedMode = 'xml'; + + expect(mode).toBe(expectedMode); + }); + + it('Returns correct json file mode', () => { + const fileName = 'file1.json'; + const mode = getFileMode(fileName); + const expectedMode = 'application/json'; + + expect(mode).toBe(expectedMode); + }); + + it('Returns correct frag|glsl file mode', () => { + const fileName = 'file1.frag'; + const fileName2 = 'file2.glsl'; + const mode = getFileMode(fileName); + const mode2 = getFileMode(fileName2); + const expectedMode = 'x-shader/x-fragment'; + + expect(mode).toBe(expectedMode); + expect(mode2).toBe(expectedMode); + }); + + it('Returns correct vert|stl|mtl file mode', () => { + const fileName = 'file1.vert'; + const fileName2 = 'file2.stl'; + const fileName3 = 'file3.mtl'; + const mode = getFileMode(fileName); + const mode2 = getFileMode(fileName2); + const mode3 = getFileMode(fileName3); + const expectedMode = 'x-shader/x-vertex'; + + expect(mode).toBe(expectedMode); + expect(mode2).toBe(expectedMode); + expect(mode3).toBe(expectedMode); + }); + + it('Returns plain text otherwise file mode', () => { + const fileName = 'file1.py'; + const mode = getFileMode(fileName); + const expectedMode = 'text/plain'; + expect(mode).toBe(expectedMode); + }); + + it('Empty fileName', () => { + const fileName = ''; + const mode = getFileMode(fileName); + const expectedMode = 'text/plain'; + expect(mode).toBe(expectedMode); + }); + + it('Unknown fileName', () => { + const fileName = 'file1.xyz'; + const mode = getFileMode(fileName); + const expectedMode = 'text/plain'; + expect(mode).toBe(expectedMode); + }); +}); + +describe('createNewFileState', () => { + function getDocText(cmView) { + return cmView.state.doc.toString(); + } + + it('Enables line wrap', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: true, + lineNumbers: true, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-lineWrapping'); + + expect(div).not.toBeNull(); + }); + + it('Enables line wrap', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: true, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-lineWrapping'); + + expect(div).toBeNull(); + }); + + it('Enables line numbers', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: true, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-lineNumbers'); + + expect(div).not.toBeNull(); + }); + + it('Disable line numbers', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-lineNumbers'); + + expect(div).toBeNull(); + }); + + it('Enable autocomplete', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: true, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-content'); + + expect(div).toHaveAttribute('aria-autocomplete', 'list'); + }); + + it('Disable autocomplete', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + const result = createNewFileState(fileName, content, settings); + + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + const div = parent.querySelector('.cm-content'); + + expect(div).not.toHaveAttribute('aria-autocomplete', 'list'); + }); + + it('Enables autoclose brackets and quotes', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: true, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + + const result = createNewFileState(fileName, content, settings); + + expect(result.closeBracketsCpt.get(result.cmState).length).toBeGreaterThan( + 0 + ); + }); + + it('Disable autoclose brackets and quotes', () => { + const fileName = 'file1.js'; + const content = ``; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + + const result = createNewFileState(fileName, content, settings); + + expect(result.closeBracketsCpt.get(result.cmState).length).toBe(0); + }); + + it('Tidy code keymap defined', () => { + const tidySpy = jest + .spyOn(tidier, 'tidyCodeWithPrettier') + .mockImplementation(() => {}); + + const fileName = 'file1.css'; + const content = `h1{color: blue;}`; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + + const result = createNewFileState(fileName, content, settings); + + const cmView = new EditorView({ state: result.cmState }); + + const km = result.cmState + .facet(keymap) + .flat() + .find((k) => k.key === 'Shift-Mod-F'); + + expect(km).toBeDefined(); + tidySpy.mockRestore(); + }); + + it('Tidy code keymap calls correct function', () => { + const tidySpy = jest + .spyOn(tidier, 'tidyCodeWithPrettier') + .mockImplementation(() => {}); + + const fileName = 'file1.css'; + const content = `h1{color: blue;}`; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + + const result = createNewFileState(fileName, content, settings); + + const cmView = new EditorView({ state: result.cmState }); + + const km = result.cmState + .facet(keymap) + .flat() + .find((k) => k.key === 'Shift-Mod-F'); + + km.run(cmView); + + expect(tidySpy).toHaveBeenCalledWith(cmView, expect.any(String)); + tidySpy.mockRestore(); + }); + + it('Tidy code keymap correctly formats code', () => { + const tidySpy = jest + .spyOn(tidier, 'tidyCodeWithPrettier') + .mockImplementation(() => {}); + + const fileName = 'file1.css'; + const content = `h1{color: blue;}`; + const formattedContent = `h1 {\n color: blue;\n}`; + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn() + }; + + const result = createNewFileState(fileName, content, settings); + + const cmView = new EditorView({ state: result.cmState }); + + const km = result.cmState + .facet(keymap) + .flat() + .find((k) => k.key === 'Shift-Mod-F'); + + km.run(cmView); + + expect(getDocText(cmView)).toBe(formattedContent); + tidySpy.mockRestore(); + }); +}); + +function makeView(fileName, content = '', extraSettings = {}) { + const settings = { + linewrap: false, + lineNumbers: false, + autocomplete: false, + autocloseBracketsQuotes: false, + onUpdateLinting: jest.fn(), + onViewUpdate: jest.fn(), + ...extraSettings + }; + const result = createNewFileState(fileName, content, settings); + const parent = document.createElement('div'); + const cmView = new EditorView({ state: result.cmState, parent }); + return { cmView, result, settings }; +} + +describe('getFileLanguage (indirect)', () => { + it('Loads a language extension for JS files without throwing', () => { + expect(() => makeView('sketch.js', 'var x = 1;')).not.toThrow(); + }); + + it('Loads a language extension for CSS files without throwing', () => { + expect(() => makeView('style.css', 'body { color: red; }')).not.toThrow(); + }); + + it('Loads a language extension for HTML files without throwing', () => { + expect(() => makeView('index.html', '
')).not.toThrow(); + }); + + it('Loads a language extension for XML files without throwing', () => { + expect(() => makeView('data.xml', 'Hello