/* eslint-disable max-statements */ import SequenceDiagram from '../SequenceDiagram.mjs'; const CM = window.CodeMirror; describe('Code Mirror Mode', () => { SequenceDiagram.registerCodeMirrorMode(CM); const cm = new CM(null, { globals: { themes: ['Theme', 'Other Theme'], }, mode: 'sequence', value: '', }); function getTokens(line) { return cm.getLineTokens(line).map((token) => ({ type: token.type, v: token.string, })); } describe('colouring', () => { it('highlights comments', () => { cm.getDoc().setValue('# foo'); expect(getTokens(0)).toEqual([ {type: 'comment', v: '# foo'}, ]); }); it('highlights valid keywords', () => { cm.getDoc().setValue('terminators cross'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'terminators'}, {type: 'keyword', v: ' cross'}, ]); }); it('highlights comments after content', () => { cm.getDoc().setValue('terminators cross # foo'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'terminators'}, {type: 'keyword', v: ' cross'}, {type: 'comment', v: ' # foo'}, ]); }); it('highlights invalid lines', () => { cm.getDoc().setValue('terminators huh'); expect(getTokens(0)[1].type).toContain('line-error'); }); it('highlights incomplete lines', () => { cm.getDoc().setValue('terminators'); expect(getTokens(0)[0].type).toContain('line-error'); }); it('highlights free text', () => { cm.getDoc().setValue('title my free text'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'title'}, {type: 'string', v: ' my'}, {type: 'string', v: ' free'}, {type: 'string', v: ' text'}, ]); }); it('highlights quoted text', () => { cm.getDoc().setValue('title "my free text"'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'title'}, {type: 'string', v: ' "my free text"'}, ]); }); it('highlights agent names', () => { cm.getDoc().setValue('A -> B'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'A'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' B'}, ]); }); it('highlights parallel statements', () => { cm.getDoc().setValue('& A -> B'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: '&'}, {type: 'variable', v: ' A'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' B'}, ]); }); it('highlights invalid parallel statements', () => { cm.getDoc().setValue('& terminators cross'); expect(getTokens(0)[2].type).toContain('line-error'); }); it('does not consider quoted tokens as keywords', () => { cm.getDoc().setValue('A "->" -> B'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'A'}, {type: 'variable', v: ' "->"'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' B'}, ]); }); it('highlights agent aliasing syntax', () => { cm.getDoc().setValue('define A as B, C as D'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'define'}, {type: 'variable', v: ' A'}, {type: 'keyword', v: ' as'}, {type: 'variable', v: ' B'}, {type: 'operator', v: ','}, {type: 'variable', v: ' C'}, {type: 'keyword', v: ' as'}, {type: 'variable', v: ' D'}, ]); }); it('highlights multi-word agent names', () => { cm.getDoc().setValue('Foo Bar -> Zig Zag'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'Foo'}, {type: 'variable', v: ' Bar'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' Zig'}, {type: 'variable', v: ' Zag'}, ]); }); it('highlights connection operators without spaces', () => { cm.getDoc().setValue('abc->xyz'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'abc'}, {type: 'keyword', v: '->'}, {type: 'variable', v: 'xyz'}, ]); }); it('highlights the lost message operator without spaces', () => { cm.getDoc().setValue('abc-xxyz'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'abc'}, {type: 'keyword', v: '-x'}, {type: 'variable', v: 'xyz'}, ]); }); it('recognises agent flags on the right', () => { cm.getDoc().setValue('Foo -> *Bar'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'Foo'}, {type: 'keyword', v: ' ->'}, {type: 'operator', v: ' *'}, {type: 'variable', v: 'Bar'}, ]); }); it('recognises agent flags on the left', () => { cm.getDoc().setValue('*Foo -> Bar'); expect(getTokens(0)).toEqual([ {type: 'operator', v: '*'}, {type: 'variable', v: 'Foo'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' Bar'}, ]); }); it('rejects missing agent names', () => { cm.getDoc().setValue('+ -> Bar'); expect(getTokens(0)[2].type).toContain('line-error'); cm.getDoc().setValue('Bar -> +'); expect(getTokens(0)[2].type).toContain('line-error'); }); it('recognises found messages', () => { cm.getDoc().setValue('* -> Bar'); expect(getTokens(0)[2].type).not.toContain('line-error'); cm.getDoc().setValue('Bar <- *'); expect(getTokens(0)[2].type).not.toContain('line-error'); }); it('recognises combined agent flags', () => { cm.getDoc().setValue('Foo -> +*Bar'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'Foo'}, {type: 'keyword', v: ' ->'}, {type: 'operator', v: ' +'}, {type: 'operator', v: '*'}, {type: 'variable', v: 'Bar'}, ]); }); it('allows messages after connections', () => { cm.getDoc().setValue('Foo -> Bar: hello'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'Foo'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' Bar'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hello'}, ]); }); it('recognises invalid agent flag combinations', () => { cm.getDoc().setValue('Foo -> *!Bar'); expect(getTokens(0)[3].type).toContain('line-error'); cm.getDoc().setValue('Foo -> +-Bar'); expect(getTokens(0)[3].type).toContain('line-error'); cm.getDoc().setValue('Foo -> +*-Bar'); expect(getTokens(0)[4].type).toContain('line-error'); }); it('highlights delayed message syntax', () => { cm.getDoc().setValue('A -> ...x\n...x -> B: hello'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'A'}, {type: 'keyword', v: ' ->'}, {type: 'operator', v: ' ...'}, {type: 'variable', v: 'x'}, ]); expect(getTokens(1)).toEqual([ {type: 'operator', v: '...'}, {type: 'variable', v: 'x'}, {type: 'keyword', v: ' ->'}, {type: 'variable', v: ' B'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hello'}, ]); }); it('recognises invalid delayed messages', () => { cm.getDoc().setValue('A -> ...x: hello'); expect(getTokens(0)[4].type).toContain('line-error'); }); it('highlights block statements', () => { cm.getDoc().setValue( 'if\n' + 'if something\n' + 'else if another thing\n' + 'else\n' + 'end\n' + 'repeat a few times\n' + 'group foo\n' ); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'if'}, ]); expect(getTokens(1)).toEqual([ {type: 'keyword', v: 'if'}, {type: 'string', v: ' something'}, ]); expect(getTokens(2)).toEqual([ {type: 'keyword', v: 'else'}, {type: 'keyword', v: ' if'}, {type: 'string', v: ' another'}, {type: 'string', v: ' thing'}, ]); expect(getTokens(3)).toEqual([ {type: 'keyword', v: 'else'}, ]); expect(getTokens(4)).toEqual([ {type: 'keyword', v: 'end'}, ]); expect(getTokens(5)).toEqual([ {type: 'keyword', v: 'repeat'}, {type: 'string', v: ' a'}, {type: 'string', v: ' few'}, {type: 'string', v: ' times'}, ]); expect(getTokens(6)).toEqual([ {type: 'keyword', v: 'group'}, {type: 'string', v: ' foo'}, ]); }); it('allows colons in block statements', () => { cm.getDoc().setValue( 'if: something\n' + 'else if: another thing\n' + 'repeat: a few times' ); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'if'}, {type: 'operator', v: ':'}, {type: 'string', v: ' something'}, ]); expect(getTokens(1)).toEqual([ {type: 'keyword', v: 'else'}, {type: 'keyword', v: ' if'}, {type: 'operator', v: ':'}, {type: 'string', v: ' another'}, {type: 'string', v: ' thing'}, ]); expect(getTokens(2)).toEqual([ {type: 'keyword', v: 'repeat'}, {type: 'operator', v: ':'}, {type: 'string', v: ' a'}, {type: 'string', v: ' few'}, {type: 'string', v: ' times'}, ]); }); it('highlights note statements', () => { cm.getDoc().setValue( 'note over A: hi\n' + 'note over A, B: hi\n' + 'note left of A, B: hi\n' + 'note right of A, B: hi\n' + 'note between A, B: hi' ); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'note'}, {type: 'keyword', v: ' over'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); expect(getTokens(1)).toEqual([ {type: 'keyword', v: 'note'}, {type: 'keyword', v: ' over'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ','}, {type: 'variable', v: ' B'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); expect(getTokens(2)).toEqual([ {type: 'keyword', v: 'note'}, {type: 'keyword', v: ' left'}, {type: 'keyword', v: ' of'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ','}, {type: 'variable', v: ' B'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); expect(getTokens(3)).toEqual([ {type: 'keyword', v: 'note'}, {type: 'keyword', v: ' right'}, {type: 'keyword', v: ' of'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ','}, {type: 'variable', v: ' B'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); expect(getTokens(4)).toEqual([ {type: 'keyword', v: 'note'}, {type: 'keyword', v: ' between'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ','}, {type: 'variable', v: ' B'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); }); it('rejects notes between a single agent', () => { cm.getDoc().setValue('note between A: hi'); expect(getTokens(0)[3].type).toContain('line-error'); }); it('highlights state statements', () => { cm.getDoc().setValue('state over A: hi'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'state'}, {type: 'keyword', v: ' over'}, {type: 'variable', v: ' A'}, {type: 'operator', v: ':'}, {type: 'string', v: ' hi'}, ]); }); it('rejects state over multiple agents', () => { cm.getDoc().setValue('state over A, B: hi'); expect(getTokens(0)[3].type).toContain('line-error'); }); it('highlights divider statements', () => { cm.getDoc().setValue('divider tear with height 60: stuff'); expect(getTokens(0)).toEqual([ {type: 'keyword', v: 'divider'}, {type: 'keyword', v: ' tear'}, {type: 'keyword', v: ' with'}, {type: 'keyword', v: ' height'}, {type: 'number', v: ' 60'}, {type: 'operator', v: ':'}, {type: 'string', v: ' stuff'}, ]); }); it('highlights agent info statements', () => { cm.getDoc().setValue('A is a red database'); expect(getTokens(0)).toEqual([ {type: 'variable', v: 'A'}, {type: 'keyword', v: ' is'}, {type: 'keyword', v: ' a'}, {type: 'keyword', v: ' red'}, {type: 'keyword', v: ' database'}, ]); }); it('rejects unknown info statements', () => { cm.getDoc().setValue('A is a foobar'); expect(getTokens(0)[3].type).toContain('line-error'); }); it('resets error handling on new lines with comments', () => { cm.getDoc().setValue('nope\n#foo'); expect(getTokens(0)[0].type).toContain('line-error'); expect(getTokens(1)[0].type).not.toContain('line-error'); }); }); describe('autocomplete', () => { function getHints(pos, {completeSingle = true} = {}) { const [hintFn] = cm.getHelpers(pos, 'hint'); cm.setCursor(pos); return hintFn(cm, Object.assign({completeSingle}, cm.options)); } function getHintTexts(pos, options) { const hints = getHints(pos, options); return hints.list.map((hint) => hint.text); } it('suggests commands when used at the start of a line', () => { cm.getDoc().setValue(''); const hints = getHintTexts({ch: 0, line: 0}); expect(hints).toContain('theme '); expect(hints).toContain('title '); expect(hints).toContain('headers '); expect(hints).toContain('terminators '); expect(hints).toContain('divider '); expect(hints).toContain('define '); expect(hints).toContain('begin '); expect(hints).toContain('end '); expect(hints).toContain('if '); expect(hints).toContain('else\n'); expect(hints).toContain('else if: '); expect(hints).toContain('repeat '); expect(hints).toContain('group '); expect(hints).toContain('note '); expect(hints).toContain('state over '); expect(hints).toContain('text '); expect(hints).toContain('autolabel '); expect(hints).toContain('simultaneously '); }); it('ignores indentation', () => { cm.getDoc().setValue(' '); const hints = getHintTexts({ch: 2, line: 0}); expect(hints).toContain('theme '); expect(hints).toContain('title '); }); it('suggests known header types', () => { cm.getDoc().setValue('headers '); const hints = getHintTexts({ch: 8, line: 0}); expect(hints).toEqual([ 'none\n', 'cross\n', 'box\n', 'fade\n', 'bar\n', ]); }); it('suggests known terminator types', () => { cm.getDoc().setValue('terminators '); const hints = getHintTexts({ch: 12, line: 0}); expect(hints).toEqual([ 'none\n', 'cross\n', 'box\n', 'fade\n', 'bar\n', ]); }); it('suggests divider types', () => { cm.getDoc().setValue('divider '); const hints = getHintTexts({ch: 8, line: 0}); expect(hints).toEqual([ 'line ', 'space ', 'delay ', 'tear ', '\n', ': ', 'with height ', ]); }); it('suggests divider sizes', () => { cm.getDoc().setValue('divider space with height '); const hints = getHintTexts({ch: 26, line: 0}); expect(hints).toEqual([ '6 ', '30 ', ]); }); it('suggests useful autolabel values', () => { cm.getDoc().setValue('autolabel '); const hints = getHintTexts({ch: 10, line: 0}); expect(hints).toContain('off\n'); expect(hints).toContain('"