SequenceDiagram/scripts/sequence/codemirror/Mode_webspec.mjs

749 lines
19 KiB
JavaScript

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('"<label>"\n');
expect(hints).toContain('"[<inc>] <label>"\n');
});
it('suggests note positioning', () => {
cm.getDoc().setValue('note ');
const hints = getHintTexts({ch: 5, line: 0});
expect(hints).toEqual([
'over ',
'left of ',
'left: ',
'right of ',
'right: ',
'between ',
]);
});
it('filters suggestions', () => {
cm.getDoc().setValue('term');
const hints = getHintTexts({ch: 4, line: 0});
expect(hints).toEqual(['terminators ']);
});
it('suggests known agent names and flags', () => {
cm.getDoc().setValue('Foo -> ');
const hints = getHintTexts({ch: 7, line: 0});
expect(hints).toEqual([
'+ ',
'- ',
'* ',
'! ',
'Foo ',
'... ',
]);
});
it('only suggests valid flag combinations', () => {
cm.getDoc().setValue('Foo -> + ');
const hints = getHintTexts({ch: 10, line: 0});
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({ch: 0, line: 1});
expect(hints).toContain('Foo ');
expect(hints).toContain('Bar ');
});
it('suggests known labels', () => {
cm.getDoc().setValue('Abc:\nsimultaneously with ');
const hints = getHintTexts({ch: 20, line: 1});
expect(hints).toEqual(['Abc ']);
});
it('suggests known themes', () => {
cm.getDoc().setValue('theme ');
const hints = getHintTexts({ch: 6, line: 0});
expect(hints).toEqual(['Theme\n', 'Other Theme\n']);
});
it('suggests filtered multi-word themes', () => {
cm.getDoc().setValue('theme Other ');
const hints = getHintTexts({ch: 12, line: 0});
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({ch: 11, line: 1});
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({ch: 12, line: 1});
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({ch: 15, line: 1});
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({ch: 16, line: 0});
expect(hints).toContain('"Zig -> Zag" ');
});
it('filters quoted names ignoring quotes', () => {
cm.getDoc().setValue('"Zig -> Zag" -> Zig');
let hints = getHintTexts({ch: 19, line: 0});
expect(hints).toContain('"Zig -> Zag" ');
cm.getDoc().setValue('"Zig -> Zag" -> Zag');
hints = getHintTexts({ch: 19, line: 0});
expect(hints).not.toContain('"Zig -> Zag" ');
});
it('suggests known delayed agents', () => {
cm.getDoc().setValue('A -> ...woo\n... ');
const hints = getHintTexts({ch: 4, line: 1});
expect(hints).toContain('woo ');
});
it('suggests anonymous delayed agents', () => {
cm.getDoc().setValue('A -> ...\n... ');
const hints = getHintTexts({ch: 4, line: 1});
expect(hints).toContain('-> ');
});
it('suggests agent properties', () => {
cm.getDoc().setValue('A is a ');
const hints = getHintTexts({ch: 7, line: 0});
expect(hints).toContain('person ');
expect(hints).toContain('database ');
expect(hints).toContain('red ');
expect(hints).not.toContain('\n');
});
it('suggests indefinite articles for agent properties', () => {
cm.getDoc().setValue('A is ');
const hints = getHintTexts({ch: 5, line: 0});
expect(hints).toContain('database ');
expect(hints).toContain('a ');
expect(hints).toContain('an ');
expect(hints).not.toContain('\n');
});
it('suggests more agent properties after the first', () => {
cm.getDoc().setValue('A is a red ');
const hints = getHintTexts({ch: 11, line: 0});
expect(hints).toContain('database ');
expect(hints).toContain('\n');
expect(hints).not.toContain('a ');
expect(hints).not.toContain('an ');
});
});
});