SequenceDiagram/scripts/sequence/CodeMirrorMode_spec.js

624 lines
16 KiB
JavaScript

defineDescribe('Code Mirror Mode', [
'./SequenceDiagram',
'cm/lib/codemirror-real',
], (
SequenceDiagram,
CodeMirror
) => {
'use strict';
SequenceDiagram.registerCodeMirrorMode(CodeMirror);
const cm = new CodeMirror(null, {
value: '',
mode: 'sequence',
globals: {
themes: ['Theme', 'Other Theme'],
},
});
function getTokens(line) {
return cm.getLineTokens(line).map((token) => ({
v: token.string,
type: token.type,
}));
}
describe('colouring', () => {
it('highlights comments', () => {
cm.getDoc().setValue('# foo');
expect(getTokens(0)).toEqual([
{v: '# foo', type: 'comment'},
]);
});
it('highlights valid keywords', () => {
cm.getDoc().setValue('terminators cross');
expect(getTokens(0)).toEqual([
{v: 'terminators', type: 'keyword'},
{v: ' cross', type: 'keyword'},
]);
});
it('highlights comments after content', () => {
cm.getDoc().setValue('terminators cross # foo');
expect(getTokens(0)).toEqual([
{v: 'terminators', type: 'keyword'},
{v: ' cross', type: 'keyword'},
{v: ' # foo', type: 'comment'},
]);
});
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([
{v: 'title', type: 'keyword'},
{v: ' my', type: 'string'},
{v: ' free', type: 'string'},
{v: ' text', type: 'string'},
]);
});
it('highlights quoted text', () => {
cm.getDoc().setValue('title "my free text"');
expect(getTokens(0)).toEqual([
{v: 'title', type: 'keyword'},
{v: ' "my free text"', type: 'string'},
]);
});
it('highlights agent names', () => {
cm.getDoc().setValue('A -> B');
expect(getTokens(0)).toEqual([
{v: 'A', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' B', type: 'variable'},
]);
});
it('does not consider quoted tokens as keywords', () => {
cm.getDoc().setValue('A "->" -> B');
expect(getTokens(0)).toEqual([
{v: 'A', type: 'variable'},
{v: ' "->"', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' B', type: 'variable'},
]);
});
it('highlights agent aliasing syntax', () => {
cm.getDoc().setValue('define A as B, C as D');
expect(getTokens(0)).toEqual([
{v: 'define', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ' as', type: 'keyword'},
{v: ' B', type: 'variable'},
{v: ',', type: 'operator'},
{v: ' C', type: 'variable'},
{v: ' as', type: 'keyword'},
{v: ' D', type: 'variable'},
]);
});
it('highlights multi-word agent names', () => {
cm.getDoc().setValue('Foo Bar -> Zig Zag');
expect(getTokens(0)).toEqual([
{v: 'Foo', type: 'variable'},
{v: ' Bar', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' Zig', type: 'variable'},
{v: ' Zag', type: 'variable'},
]);
});
it('highlights connection operators without spaces', () => {
cm.getDoc().setValue('abc->xyz');
expect(getTokens(0)).toEqual([
{v: 'abc', type: 'variable'},
{v: '->', type: 'keyword'},
{v: 'xyz', type: 'variable'},
]);
});
it('highlights the lost message operator without spaces', () => {
cm.getDoc().setValue('abc-xxyz');
expect(getTokens(0)).toEqual([
{v: 'abc', type: 'variable'},
{v: '-x', type: 'keyword'},
{v: 'xyz', type: 'variable'},
]);
});
it('recognises agent flags on the right', () => {
cm.getDoc().setValue('Foo -> *Bar');
expect(getTokens(0)).toEqual([
{v: 'Foo', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' *', type: 'operator'},
{v: 'Bar', type: 'variable'},
]);
});
it('recognises agent flags on the left', () => {
cm.getDoc().setValue('*Foo -> Bar');
expect(getTokens(0)).toEqual([
{v: '*', type: 'operator'},
{v: 'Foo', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' Bar', type: 'variable'},
]);
});
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([
{v: 'Foo', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' +', type: 'operator'},
{v: '*', type: 'operator'},
{v: 'Bar', type: 'variable'},
]);
});
it('allows messages after connections', () => {
cm.getDoc().setValue('Foo -> Bar: hello');
expect(getTokens(0)).toEqual([
{v: 'Foo', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' Bar', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hello', type: 'string'},
]);
});
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([
{v: 'A', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' ...', type: 'operator'},
{v: 'x', type: 'variable'},
]);
expect(getTokens(1)).toEqual([
{v: '...', type: 'operator'},
{v: 'x', type: 'variable'},
{v: ' ->', type: 'keyword'},
{v: ' B', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hello', type: 'string'},
]);
});
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([
{v: 'if', type: 'keyword'},
]);
expect(getTokens(1)).toEqual([
{v: 'if', type: 'keyword'},
{v: ' something', type: 'string'},
]);
expect(getTokens(2)).toEqual([
{v: 'else', type: 'keyword'},
{v: ' if', type: 'keyword'},
{v: ' another', type: 'string'},
{v: ' thing', type: 'string'},
]);
expect(getTokens(3)).toEqual([
{v: 'else', type: 'keyword'},
]);
expect(getTokens(4)).toEqual([
{v: 'end', type: 'keyword'},
]);
expect(getTokens(5)).toEqual([
{v: 'repeat', type: 'keyword'},
{v: ' a', type: 'string'},
{v: ' few', type: 'string'},
{v: ' times', type: 'string'},
]);
expect(getTokens(6)).toEqual([
{v: 'group', type: 'keyword'},
{v: ' foo', type: 'string'},
]);
});
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([
{v: 'if', type: 'keyword'},
{v: ':', type: 'operator'},
{v: ' something', type: 'string'},
]);
expect(getTokens(1)).toEqual([
{v: 'else', type: 'keyword'},
{v: ' if', type: 'keyword'},
{v: ':', type: 'operator'},
{v: ' another', type: 'string'},
{v: ' thing', type: 'string'},
]);
expect(getTokens(2)).toEqual([
{v: 'repeat', type: 'keyword'},
{v: ':', type: 'operator'},
{v: ' a', type: 'string'},
{v: ' few', type: 'string'},
{v: ' times', type: 'string'},
]);
});
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([
{v: 'note', type: 'keyword'},
{v: ' over', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
expect(getTokens(1)).toEqual([
{v: 'note', type: 'keyword'},
{v: ' over', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ',', type: 'operator'},
{v: ' B', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
expect(getTokens(2)).toEqual([
{v: 'note', type: 'keyword'},
{v: ' left', type: 'keyword'},
{v: ' of', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ',', type: 'operator'},
{v: ' B', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
expect(getTokens(3)).toEqual([
{v: 'note', type: 'keyword'},
{v: ' right', type: 'keyword'},
{v: ' of', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ',', type: 'operator'},
{v: ' B', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
expect(getTokens(4)).toEqual([
{v: 'note', type: 'keyword'},
{v: ' between', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ',', type: 'operator'},
{v: ' B', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
});
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([
{v: 'state', type: 'keyword'},
{v: ' over', type: 'keyword'},
{v: ' A', type: 'variable'},
{v: ':', type: 'operator'},
{v: ' hi', type: 'string'},
]);
});
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([
{v: 'divider', type: 'keyword'},
{v: ' tear', type: 'keyword'},
{v: ' with', type: 'keyword'},
{v: ' height', type: 'keyword'},
{v: ' 60', type: 'number'},
{v: ':', type: 'operator'},
{v: ' stuff', type: 'string'},
]);
});
});
describe('autocomplete', () => {
function getHints(pos, {completeSingle = true} = {}) {
const hintFn = cm.getHelpers(pos, 'hint')[0];
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({line: 0, ch: 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({line: 0, ch: 2});
expect(hints).toContain('theme ');
expect(hints).toContain('title ');
});
it('suggests known header types', () => {
cm.getDoc().setValue('headers ');
const hints = getHintTexts({line: 0, ch: 8});
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({line: 0, ch: 12});
expect(hints).toEqual([
'none\n',
'cross\n',
'box\n',
'fade\n',
'bar\n',
]);
});
it('suggests divider types', () => {
cm.getDoc().setValue('divider ');
const hints = getHintTexts({line: 0, ch: 8});
expect(hints).toEqual([
'line ',
'space ',
'delay ',
'tear ',
'\n',
': ',
'with height ',
]);
});
it('suggests divider sizes', () => {
cm.getDoc().setValue('divider space with height ');
const hints = getHintTexts({line: 0, ch: 26});
expect(hints).toEqual([
'6 ',
'30 ',
]);
});
it('suggests useful autolabel values', () => {
cm.getDoc().setValue('autolabel ');
const hints = getHintTexts({line: 0, ch: 10});
expect(hints).toContain('off\n');
expect(hints).toContain('"<label>"\n');
expect(hints).toContain('"[<inc>] <label>"\n');
});
it('suggests note positioning', () => {
cm.getDoc().setValue('note ');
const hints = getHintTexts({line: 0, ch: 5});
expect(hints).toEqual([
'over ',
'left of ',
'left: ',
'right of ',
'right: ',
'between ',
]);
});
it('filters suggestions', () => {
cm.getDoc().setValue('term');
const hints = getHintTexts({line: 0, ch: 4});
expect(hints).toEqual(['terminators ']);
});
it('suggests known agent names and flags', () => {
cm.getDoc().setValue('Foo -> ');
const hints = getHintTexts({line: 0, ch: 7});
expect(hints).toEqual([
'+ ',
'- ',
'* ',
'! ',
'Foo ',
'... ',
]);
});
it('only suggests valid flag combinations', () => {
cm.getDoc().setValue('Foo -> + ');
const hints = getHintTexts({line: 0, ch: 10});
expect(hints).toContain('* ');
expect(hints).not.toContain('! ');
expect(hints).not.toContain('+ ');
expect(hints).not.toContain('- ');
expect(hints).toContain('Foo ');
});
it('suggests known agent names at the start of lines', () => {
cm.getDoc().setValue('Foo -> Bar\n');
const hints = getHintTexts({line: 1, ch: 0});
expect(hints).toContain('Foo ');
expect(hints).toContain('Bar ');
});
it('suggests known labels', () => {
cm.getDoc().setValue('Abc:\nsimultaneously with ');
const hints = getHintTexts({line: 1, ch: 20});
expect(hints).toEqual(['Abc ']);
});
it('suggests known themes', () => {
cm.getDoc().setValue('theme ');
const hints = getHintTexts({line: 0, ch: 6});
expect(hints).toEqual(['Theme\n', 'Other Theme\n']);
});
it('suggests filtered multi-word themes', () => {
cm.getDoc().setValue('theme Other ');
const hints = getHintTexts({line: 0, ch: 12});
expect(hints).toContain('Other Theme\n');
expect(hints).not.toContain('Theme\n');
});
it('suggests multi-word agents', () => {
cm.getDoc().setValue('Zig Zag -> Meh\nFoo Bar -> ');
const hints = getHintTexts({line: 1, ch: 11});
expect(hints).toContain('Zig Zag ');
expect(hints).toContain('Meh ');
expect(hints).toContain('Foo Bar ');
});
it('suggests quoted agent names if a quote is typed', () => {
cm.getDoc().setValue('Zig Zag -> Meh\nFoo Bar -> "');
const hints = getHintTexts({line: 1, ch: 12});
expect(hints).toEqual([
'"Zig Zag" ',
'"Meh" ',
'"Foo Bar" ',
]);
});
it('suggests filtered multi-word agents', () => {
cm.getDoc().setValue('Zig Zag -> Meh\nFoo Bar -> Foo ');
const hints = getHintTexts({line: 1, ch: 15});
expect(hints).toContain('Foo Bar ');
expect(hints).not.toContain('Zig Zag ');
expect(hints).not.toContain('Meh ');
});
it('suggests quoted names where required', () => {
cm.getDoc().setValue('"Zig -> Zag" -> ');
const hints = getHintTexts({line: 0, ch: 16});
expect(hints).toContain('"Zig -> Zag" ');
});
it('filters quoted names ignoring quotes', () => {
cm.getDoc().setValue('"Zig -> Zag" -> Zig');
let hints = getHintTexts({line: 0, ch: 19});
expect(hints).toContain('"Zig -> Zag" ');
cm.getDoc().setValue('"Zig -> Zag" -> Zag');
hints = getHintTexts({line: 0, ch: 19});
expect(hints).not.toContain('"Zig -> Zag" ');
});
it('suggests known delayed agents', () => {
cm.getDoc().setValue('A -> ...woo\n... ');
const hints = getHintTexts({line: 1, ch: 4});
expect(hints).toEqual(['woo ']);
});
});
});