/* eslint-disable max-lines */ import Parser from './Parser.mjs'; describe('Sequence Parser', () => { const parser = new Parser(); function makeParsedAgents(source) { return source.map((item) => { const base = {alias: '', flags: [], name: ''}; if(typeof item === 'object') { return Object.assign(base, item); } else { return Object.assign(base, {name: item}); } }); } const any = () => jasmine.anything(); const PARSED = { agentActivation: (agents, { ln = any(), activated = any(), parallel = false, } = {}) => ({ activated, agents: makeParsedAgents(agents), ln, parallel, type: 'agent activation', }), agentBegin: (agents, { ln = any(), mode = any(), parallel = false, } = {}) => ({ agents: makeParsedAgents(agents), ln, mode, parallel, type: 'agent begin', }), agentDefine: (agents, { ln = any(), parallel = false, } = {}) => ({ agents: makeParsedAgents(agents), ln, parallel, type: 'agent define', }), agentEnd: (agents, { ln = any(), mode = any(), parallel = false, } = {}) => ({ agents: makeParsedAgents(agents), ln, mode, parallel, type: 'agent end', }), agentOptions: (agent, options, { ln = any(), parallel = false, } = {}) => ({ agent: makeParsedAgents([agent])[0], ln, options, parallel, type: 'agent options', }), async: (target, { ln = any(), parallel = false, } = {}) => ({ ln, parallel, target, type: 'async', }), blockBegin: (tag, label, { blockType = null, ln = any(), parallel = false, } = {}) => ({ blockType: blockType || tag, label, ln, parallel, tag, type: 'block begin', }), blockEnd: ({ ln = any(), parallel = false, } = {}) => ({ ln, parallel, type: 'block end', }), blockSplit: (tag, label, { blockType = null, ln = any(), parallel = false, } = {}) => ({ blockType: blockType || tag, label, ln, parallel, tag, type: 'block split', }), connect: (agents, { label = any(), left = any(), line = any(), ln = any(), parallel = false, right = any(), } = {}) => ({ agents: makeParsedAgents(agents), label, ln, options: {left, line, right}, parallel, type: 'connect', }), connectBegin: (agent, tag, { left = any(), line = any(), ln = any(), parallel = false, right = any(), } = {}) => ({ agent: makeParsedAgents([agent])[0], ln, options: {left, line, right}, parallel, tag, type: 'connect-delay-begin', }), connectEnd: (agent, tag, { label = any(), left = any(), line = any(), ln = any(), parallel = false, right = any(), } = {}) => ({ agent: makeParsedAgents([agent])[0], label, ln, options: {left, line, right}, parallel, tag, type: 'connect-delay-end', }), divider: ({ height = any(), label = any(), ln = any(), mode = any(), parallel = false, } = {}) => ({ height, label, ln, mode, parallel, type: 'divider', }), groupBegin: (agents, { alias = any(), blockType = any(), label = any(), ln = any(), parallel = false, tag = any(), } = {}) => ({ agents: makeParsedAgents(agents), alias, blockType, label, ln, parallel, tag, type: 'group begin', }), labelPattern: (pattern, { ln = any(), parallel = false, } = {}) => ({ ln, parallel, pattern, type: 'label pattern', }), mark: (name, { ln = any(), parallel = false, } = {}) => ({ ln, name, parallel, type: 'mark', }), note: (position, agents, { label = any(), ln = any(), mode = 'note', parallel = false, } = {}) => ({ agents: makeParsedAgents(agents), label, ln, mode, parallel, type: 'note ' + position, }), }; describe('.parse', () => { it('returns an empty sequence for blank input', () => { const parsed = parser.parse(''); expect(parsed).toEqual({ meta: { code: '', headers: 'box', terminators: 'none', textFormatter: any(), theme: '', title: '', }, stages: [], }); }); it('reads title metadata', () => { const parsed = parser.parse('title foo'); expect(parsed.meta.title).toEqual('foo'); }); it('reads theme metadata', () => { const parsed = parser.parse('theme foo'); expect(parsed.meta.theme).toEqual('foo'); }); it('propagates original source as metadata', () => { const parsed = parser.parse('theme foo'); expect(parsed.meta.code).toEqual('theme foo'); }); it('reads terminators metadata', () => { const parsed = parser.parse('terminators bar'); expect(parsed.meta.terminators).toEqual('bar'); }); it('reads headers metadata', () => { const parsed = parser.parse('headers bar'); expect(parsed.meta.headers).toEqual('bar'); }); it('propagates a function which can be used to format text', () => { const parsed = parser.parse('title foo'); expect(parsed.meta.textFormatter).toEqual(jasmine.any(Function)); }); it('reads multiple tokens as one when reading values', () => { const parsed = parser.parse('title foo bar'); expect(parsed.meta.title).toEqual('foo bar'); }); it('converts entries into abstract form', () => { const parsed = parser.parse('A -> B'); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B']), ]); }); it('combines multiple tokens into single entries', () => { const parsed = parser.parse('A B -> C D'); expect(parsed.stages).toEqual([ PARSED.connect(['A B', 'C D']), ]); }); it('propagates aliases', () => { const parsed = parser.parse('define Foo Bar as A'); expect(parsed.stages).toEqual([ PARSED.agentDefine([ {alias: 'A', name: 'Foo Bar'}, ]), ]); }); it('propagates long aliases', () => { const parsed = parser.parse('define Foo Bar as A B'); expect(parsed.stages).toEqual([ PARSED.agentDefine([ {alias: 'A B', name: 'Foo Bar'}, ]), ]); }); it('ignores missing aliases', () => { const parsed = parser.parse('define Foo Bar as'); expect(parsed.stages).toEqual([ PARSED.agentDefine(['Foo Bar']), ]); }); it('propagates agent options', () => { const parsed = parser.parse('Foo bar is zig zag'); expect(parsed.stages).toEqual([ PARSED.agentOptions('Foo bar', ['zig', 'zag']), ]); }); it('ignores indefinite articles in agent options', () => { const parsed = parser.parse('Foo is a zig\nBar is an oom'); expect(parsed.stages).toEqual([ PARSED.agentOptions('Foo', ['zig']), PARSED.agentOptions('Bar', ['oom']), ]); }); it('rejects empty agent options', () => { expect(() => parser.parse('Foo is')).toThrow(new Error( 'Empty agent options at line 1, character 6' )); expect(() => parser.parse('Foo is a')).toThrow(new Error( 'Empty agent options at line 1, character 8' )); }); it('respects spacing within agent names', () => { const parsed = parser.parse('A+B -> C D'); expect(parsed.stages).toEqual([ PARSED.connect(['A+B', 'C D']), ]); }); it('parses optional labels', () => { const parsed = parser.parse('A B -> C D: foo bar'); expect(parsed.stages).toEqual([ PARSED.connect(['A B', 'C D'], {label: 'foo bar'}), ]); }); it('parses optional flags', () => { const parsed = parser.parse('+A -> -*!B'); expect(parsed.stages).toEqual([ PARSED.connect([ {flags: ['start'], name: 'A'}, {flags: ['stop', 'begin', 'end'], name: 'B'}, ]), ]); }); it('rejects duplicate flags', () => { expect(() => parser.parse('A -> +*+B')).toThrow(new Error( 'Duplicate agent flag: + at line 1, character 7' )); expect(() => parser.parse('A -> **B')).toThrow(new Error( 'Duplicate agent flag: * at line 1, character 6' )); }); it('rejects missing agent names', () => { expect(() => parser.parse('A -> +')).toThrow(new Error( 'Missing agent name at line 1, character 6' )); }); it('rejects missing agent names with aliases', () => { expect(() => parser.parse('define as A')).toThrow(new Error( 'Missing agent name at line 1, character 7' )); }); it('parses source agents', () => { const parsed = parser.parse('A -> *'); expect(parsed.stages).toEqual([ PARSED.connect(['A', {flags: ['source'], name: ''}]), ]); }); it('parses source agents with labels', () => { const parsed = parser.parse('A -> *: foo'); expect(parsed.stages).toEqual([ PARSED.connect(['A', {flags: ['source'], name: ''}], { label: 'foo', }), ]); }); it('converts multiple entries', () => { const parsed = parser.parse('A -> B\nB -> A'); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B']), PARSED.connect(['B', 'A']), ]); }); it('ignores blank lines', () => { const parsed = parser.parse('A -> B\n\nB -> A\n'); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B']), PARSED.connect(['B', 'A']), ]); }); it('stores line numbers', () => { const parsed = parser.parse('A -> B\nB -> A'); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B'], {ln: 0}), PARSED.connect(['B', 'A'], {ln: 1}), ]); }); it('recognises all types of connection', () => { const parsed = parser.parse( 'A->B\n' + 'A->>B\n' + 'A-~B\n' + 'A-xB\n' + 'A<-B\n' + 'A<->B\n' + 'A<->>B\n' + 'A<-~B\n' + 'A<-xB\n' + 'A<<-B\n' + 'A<<->B\n' + 'A<<->>B\n' + 'A<<-~B\n' + 'A<<-xB\n' + 'A~-B\n' + 'A~->B\n' + 'A~->>B\n' + 'A~-~B\n' + 'A~-xB\n' + 'A-->B\n' + 'A-->>B\n' + 'A--~B\n' + 'A--xB\n' + 'A<--B\n' + 'A<-->B\n' + 'A<-->>B\n' + 'A<--~B\n' + 'A<--xB\n' + 'A<<--B\n' + 'A<<-->B\n' + 'A<<-->>B\n' + 'A<<--~B\n' + 'A<<--xB\n' + 'A~--B\n' + 'A~-->B\n' + 'A~-->>B\n' + 'A~--~B\n' + 'A~--xB\n' + 'A~>B\n' + 'A~>>B\n' + 'A~~B\n' + 'A~xB\n' + 'A<~B\n' + 'A<~>B\n' + 'A<~>>B\n' + 'A<~~B\n' + 'A<~xB\n' + 'A<<~B\n' + 'A<<~>B\n' + 'A<<~>>B\n' + 'A<<~~B\n' + 'A<<~xB\n' + 'A~~>B\n' + 'A~~>>B\n' + 'A~~~B\n' + 'A~~xB\n' ); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B'], { label: '', left: 0, line: 'solid', right: 1, }), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'solid', right: 4}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 3}), PARSED.connect(['A', 'B'], {left: 1, line: 'solid', right: 4}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'solid', right: 4}), PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 0}), PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 1}), PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 2}), PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 3}), PARSED.connect(['A', 'B'], {left: 3, line: 'solid', right: 4}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'dash', right: 4}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 3}), PARSED.connect(['A', 'B'], {left: 1, line: 'dash', right: 4}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'dash', right: 4}), PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 0}), PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 1}), PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 2}), PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 3}), PARSED.connect(['A', 'B'], {left: 3, line: 'dash', right: 4}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 3}), PARSED.connect(['A', 'B'], {left: 0, line: 'wave', right: 4}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 0}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 3}), PARSED.connect(['A', 'B'], {left: 1, line: 'wave', right: 4}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 0}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 3}), PARSED.connect(['A', 'B'], {left: 2, line: 'wave', right: 4}), PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 1}), PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 2}), PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 3}), PARSED.connect(['A', 'B'], {left: 3, line: 'wave', right: 4}), ]); }); it('ignores arrows within the label', () => { const parsed = parser.parse( 'A <- B: B -> A\n' + 'A -> B: B <- A\n' ); expect(parsed.stages).toEqual([ PARSED.connect(['A', 'B'], { label: 'B -> A', left: 1, line: 'solid', right: 0, }), PARSED.connect(['A', 'B'], { label: 'B <- A', left: 0, line: 'solid', right: 1, }), ]); }); it('converts delayed connections', () => { const parsed = parser.parse('+A <- ...foo\n...foo -> -B: woo'); expect(parsed.stages).toEqual([ PARSED.connectBegin( {flags: ['start'], name: 'A'}, 'foo', {left: 1, line: 'solid', right: 0} ), PARSED.connectEnd( {flags: ['stop'], name: 'B'}, 'foo', {label: 'woo', left: 0, line: 'solid', right: 1} ), ]); }); it('converts notes', () => { const parsed = parser.parse('note over A: hello there'); expect(parsed.stages).toEqual([ PARSED.note('over', ['A'], {label: 'hello there'}), ]); }); it('converts different note types', () => { const parsed = parser.parse( 'note left A: hello there\n' + 'note left of A: hello there\n' + 'note right A: hello there\n' + 'note right of A: hello there\n' + 'note between A, B: hi\n' ); expect(parsed.stages).toEqual([ PARSED.note('left', ['A'], {label: 'hello there'}), PARSED.note('left', ['A'], {label: 'hello there'}), PARSED.note('right', ['A'], {label: 'hello there'}), PARSED.note('right', ['A'], {label: 'hello there'}), PARSED.note('between', ['A', 'B'], {label: 'hi'}), ]); }); it('allows multiple agents for notes', () => { const parsed = parser.parse('note over A B, C D: hi'); expect(parsed.stages).toEqual([ PARSED.note('over', ['A B', 'C D'], {label: 'hi'}), ]); }); it('rejects note between for a single agent', () => { expect(() => parser.parse('note between A: hi')).toThrow(new Error( 'Too few agents for note at line 1, character 0' )); }); it('converts state', () => { const parsed = parser.parse('state over A: doing stuff'); expect(parsed.stages).toEqual([ PARSED.note('over', ['A'], { label: 'doing stuff', mode: 'state', }), ]); }); it('rejects multiple agents for state', () => { expect(() => parser.parse('state over A, B: hi')).toThrow(new Error( 'Too many agents for state at line 1, character 0' )); }); it('converts text blocks', () => { const parsed = parser.parse('text right of A: doing stuff'); expect(parsed.stages).toEqual([ PARSED.note('right', ['A'], { label: 'doing stuff', mode: 'text', }), ]); }); it('converts agent commands', () => { const parsed = parser.parse( 'define A, B\n' + 'begin A, B\n' + 'activate A, B\n' + 'deactivate A, B\n' + 'end A, B\n' ); expect(parsed.stages).toEqual([ PARSED.agentDefine(['A', 'B']), PARSED.agentBegin(['A', 'B'], {mode: 'box'}), PARSED.agentActivation(['A', 'B'], {activated: true}), PARSED.agentActivation(['A', 'B'], {activated: false}), PARSED.agentEnd(['A', 'B'], {mode: 'cross'}), ]); }); it('converts dividers', () => { const parsed = parser.parse('divider'); expect(parsed.stages).toEqual([ PARSED.divider({height: 6, label: '', mode: 'line'}), ]); }); it('converts different divider types', () => { const parsed = parser.parse( 'divider line\n' + 'divider space\n' + 'divider delay\n' + 'divider tear\n' ); expect(parsed.stages).toEqual([ PARSED.divider({height: 6, mode: 'line'}), PARSED.divider({height: 6, mode: 'space'}), PARSED.divider({height: 30, mode: 'delay'}), PARSED.divider({height: 6, mode: 'tear'}), ]); }); it('converts explicit divider heights', () => { const parsed = parser.parse( 'divider with height 40\n' + 'divider delay with height 0\n' ); expect(parsed.stages).toEqual([ PARSED.divider({height: 40, label: '', mode: 'line'}), PARSED.divider({height: 0, label: '', mode: 'delay'}), ]); }); it('converts divider labels', () => { const parsed = parser.parse( 'divider: message 1\n' + 'divider tear: message 2\n' + 'divider delay with height 40: message 3\n' ); expect(parsed.stages).toEqual([ PARSED.divider({label: 'message 1'}), PARSED.divider({label: 'message 2'}), PARSED.divider({label: 'message 3'}), ]); }); it('converts reference commands', () => { const parsed = parser.parse( 'begin reference: Foo bar as baz\n' + 'begin reference over A, B: Foo bar as baz\n' ); expect(parsed.stages).toEqual([ PARSED.groupBegin([], { alias: 'baz', blockType: 'ref', label: 'Foo bar', tag: 'ref', }), PARSED.groupBegin(['A', 'B'], { alias: 'baz', blockType: 'ref', label: 'Foo bar', tag: 'ref', }), ]); }); it('converts markers', () => { const parsed = parser.parse('abc:'); expect(parsed.stages).toEqual([ PARSED.mark('abc'), ]); }); it('converts autolabel commands', () => { const parsed = parser.parse('autolabel "foo