Add integration testing
This commit is contained in:
parent
90c4652f34
commit
d1c810e2af
|
@ -96,7 +96,8 @@ if fraud detected
|
|||
end Police
|
||||
else if sufficient funds
|
||||
ATM -> Bank: Withdraw funds
|
||||
repeat until all requested money handed over
|
||||
repeat until "all requested money
|
||||
has been handed over"
|
||||
ATM -> Person: Dispense note
|
||||
end
|
||||
else
|
||||
|
@ -172,7 +173,7 @@ A -> B
|
|||
B -> A
|
||||
|
||||
# Return to the defined marker
|
||||
# (should be interpreted as no-higher-then the marker; may still be
|
||||
# (should be interpreted as no-higher-then the marker; may be
|
||||
# pushed down to keep relative action ordering consistent)
|
||||
|
||||
simultaneously with some primary process:
|
||||
|
@ -181,8 +182,8 @@ D -> C
|
|||
end D
|
||||
C -> A
|
||||
|
||||
# The marker name is optional; using "simultaneously:" with no marker
|
||||
# will jump to the top of the entire sequence.
|
||||
# The marker name is optional; using "simultaneously:" with no
|
||||
# marker will jump to the top of the entire sequence.
|
||||
```
|
||||
|
||||
## DSL Basics
|
||||
|
|
|
@ -52,7 +52,9 @@ define([
|
|||
}
|
||||
|
||||
return class Renderer {
|
||||
constructor(theme) {
|
||||
constructor(theme, {
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
} = {}) {
|
||||
this.separationAgentCap = {
|
||||
'box': this.separationAgentCapBox.bind(this),
|
||||
'cross': this.separationAgentCapCross.bind(this),
|
||||
|
@ -110,6 +112,7 @@ define([
|
|||
this.height = 0;
|
||||
this.marks = new Map();
|
||||
this.theme = theme;
|
||||
this.SVGTextBlockClass = SVGTextBlockClass;
|
||||
this.currentSequence = null;
|
||||
this.buildStaticElements();
|
||||
}
|
||||
|
@ -132,9 +135,9 @@ define([
|
|||
this.base.appendChild(this.sections);
|
||||
this.base.appendChild(this.actionShapes);
|
||||
this.base.appendChild(this.actionLabels);
|
||||
this.title = new SVGTextBlock(this.base);
|
||||
this.title = new this.SVGTextBlockClass(this.base);
|
||||
|
||||
this.sizer = new SVGTextBlock.SizeTester(this.base);
|
||||
this.sizer = new this.SVGTextBlockClass.SizeTester(this.base);
|
||||
}
|
||||
|
||||
findExtremes(agents) {
|
||||
|
@ -397,6 +400,7 @@ define([
|
|||
labelAttrs: config.labelAttrs,
|
||||
boxLayer: this.actionShapes,
|
||||
labelLayer: this.actionLabels,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -543,6 +547,7 @@ define([
|
|||
labelAttrs: config.label.loopbackAttrs,
|
||||
boxLayer: this.mask,
|
||||
labelLayer: this.actionLabels,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
});
|
||||
const r = config.loopbackRadius;
|
||||
const x1 = (
|
||||
|
@ -611,6 +616,7 @@ define([
|
|||
labelAttrs: config.label.attrs,
|
||||
boxLayer: this.mask,
|
||||
labelLayer: this.actionLabels,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
this.actionShapes.appendChild(svg.make('line', Object.assign({
|
||||
|
@ -659,7 +665,7 @@ define([
|
|||
this.currentY += config.margin.top;
|
||||
|
||||
const y = this.currentY + config.padding.top;
|
||||
const labelNode = new SVGTextBlock(this.actionLabels, {
|
||||
const labelNode = new this.SVGTextBlockClass(this.actionLabels, {
|
||||
attrs: config.labelAttrs,
|
||||
text: label,
|
||||
y,
|
||||
|
@ -799,6 +805,7 @@ define([
|
|||
labelAttrs: config.section.mode.labelAttrs,
|
||||
boxLayer: this.blocks,
|
||||
labelLayer: this.actionLabels,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
const labelRender = SVGShapes.renderBoxedText(label, {
|
||||
|
@ -809,6 +816,7 @@ define([
|
|||
labelAttrs: config.section.label.labelAttrs,
|
||||
boxLayer: this.mask,
|
||||
labelLayer: this.actionLabels,
|
||||
SVGTextBlockClass: this.SVGTextBlockClass,
|
||||
});
|
||||
|
||||
this.currentY += (
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
/* jshint -W072 */
|
||||
defineDescribe('Sequence Renderer', [
|
||||
'./Parser',
|
||||
'./Generator',
|
||||
'./Renderer',
|
||||
'./themes/Basic',
|
||||
'stubs/SVGTextBlock',
|
||||
], (
|
||||
Parser,
|
||||
Generator,
|
||||
Renderer,
|
||||
Theme,
|
||||
SVGTextBlock
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
let parser = null;
|
||||
let generator = null;
|
||||
let renderer = null;
|
||||
let theme = null;
|
||||
|
||||
beforeEach(() => {
|
||||
theme = new Theme();
|
||||
parser = new Parser();
|
||||
generator = new Generator();
|
||||
renderer = new Renderer(theme, {SVGTextBlockClass: SVGTextBlock});
|
||||
document.body.appendChild(renderer.svg());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(renderer.svg());
|
||||
});
|
||||
|
||||
function getSimplifiedContent(r) {
|
||||
return (r.svg().outerHTML
|
||||
.replace(/<g><\/g>/g, '')
|
||||
.replace(' xmlns="http://www.w3.org/2000/svg" version="1.1"', '')
|
||||
);
|
||||
}
|
||||
|
||||
it('Renders empty diagrams without error', () => {
|
||||
const parsed = parser.parse('');
|
||||
const sequence = generator.generate(parsed);
|
||||
renderer.render(sequence);
|
||||
expect(getSimplifiedContent(renderer)).toEqual(
|
||||
'<svg width="100%" height="100%" viewBox="-5 -5 10 10">' +
|
||||
'</svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders simple metadata', () => {
|
||||
const parsed = parser.parse('title My title here');
|
||||
const sequence = generator.generate(parsed);
|
||||
renderer.render(sequence);
|
||||
|
||||
expect(getSimplifiedContent(renderer)).toEqual(
|
||||
'<svg width="100%" height="100%" viewBox="-11.5 -16 23 21">' +
|
||||
'<text' +
|
||||
' x="0"' +
|
||||
' font-family="sans-serif"' +
|
||||
' font-size="20"' +
|
||||
' line-height="1.3"' +
|
||||
' text-anchor="middle"' +
|
||||
' class="title"' +
|
||||
' y="-11">My title here</text>' +
|
||||
'</svg>'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders simple components', () => {
|
||||
const parsed = parser.parse('A -> B');
|
||||
const sequence = generator.generate(parsed);
|
||||
renderer.render(sequence);
|
||||
|
||||
const content = getSimplifiedContent(renderer);
|
||||
expect(content).toContain(
|
||||
'<svg width="100%" height="100%" viewBox="-5 -5 82 49">'
|
||||
);
|
||||
|
||||
// Agent 1
|
||||
expect(content).toContain(
|
||||
'<line x1="20.5" y1="11" x2="20.5" y2="39" class="agent-1-line"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<rect x="10" y="0" width="21" height="11"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<text x="20.5"'
|
||||
);
|
||||
|
||||
// Agent 2
|
||||
expect(content).toContain(
|
||||
'<line x1="51.5" y1="11" x2="51.5" y2="39" class="agent-2-line"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<rect x="41" y="0" width="21" height="11"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<text x="51.5"'
|
||||
);
|
||||
|
||||
// Arrow
|
||||
expect(content).toContain(
|
||||
'<line x1="20.5" y1="20" x2="50.5" y2="20"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<polygon points="46.5 16 50.5 20 46.5 24"'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders the "Simple Usage" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title Labyrinth\n' +
|
||||
'\n' +
|
||||
'Bowie -> Gremlin: You remind me of the babe\n' +
|
||||
'Gremlin -> Bowie: What babe?\n' +
|
||||
'Bowie -> Gremlin: The babe with the power\n' +
|
||||
'Gremlin -> Bowie: What power?\n' +
|
||||
'note right of Bowie, Gremlin: Most people get muddled here!\n' +
|
||||
'Bowie -> Gremlin: \'The power of voodoo\'\n' +
|
||||
'Gremlin -> Bowie: "Who-do?"\n' +
|
||||
'Bowie -> Gremlin: You do!\n' +
|
||||
'Gremlin -> Bowie: Do what?\n' +
|
||||
'Bowie -> Gremlin: Remind me of the babe!\n' +
|
||||
'\n' +
|
||||
'Bowie -> Audience: Sings\n' +
|
||||
'\n' +
|
||||
'terminators box\n'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Connection Types" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title Connection Types\n' +
|
||||
'\n' +
|
||||
'Foo -> Bar: Simple arrow\n' +
|
||||
'Foo --> Bar: Dashed arrow\n' +
|
||||
'Foo <- Bar: Reversed arrow\n' +
|
||||
'Foo <-- Bar: Reversed dashed arrow\n' +
|
||||
'Foo <-> Bar: Double arrow\n' +
|
||||
'Foo <--> Bar: Double dashed arrow\n' +
|
||||
'\n' +
|
||||
'# An arrow with no label:\n' +
|
||||
'Foo -> Bar\n' +
|
||||
'\n' +
|
||||
'Foo -> Foo: Foo talks to itself\n' +
|
||||
'\n' +
|
||||
'# Arrows leaving on the left and right of the diagram\n' +
|
||||
'[ -> Foo: From the left\n' +
|
||||
'[ <- Foo: To the left\n' +
|
||||
'Foo -> ]: To the right\n' +
|
||||
'Foo <- ]: From the right\n' +
|
||||
'[ -> ]: Left to right!\n' +
|
||||
'# (etc.)\n'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Notes & State" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title Note Placements\n' +
|
||||
'\n' +
|
||||
'note over Foo: Foo says something\n' +
|
||||
'note left of Foo: Stuff\n' +
|
||||
'note right of Bar: More stuff\n' +
|
||||
'note over Foo, Bar: "Foo and Bar\n' +
|
||||
'on multiple lines"\n' +
|
||||
'note between Foo, Bar: Link\n' +
|
||||
'\n' +
|
||||
'text right: \'Comments\\nOver here\!\'\n' +
|
||||
'\n' +
|
||||
'state over Foo: Foo is ponderous'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Logic" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title At the Bank\n' +
|
||||
'\n' +
|
||||
'begin Person, ATM, Bank\n' +
|
||||
'Person -> ATM: Request money\n' +
|
||||
'ATM -> Bank: Check funds\n' +
|
||||
'if fraud detected\n' +
|
||||
' Bank -> Police: "Get \'em!"\n' +
|
||||
' Police -> Person: "You\'re nicked"\n' +
|
||||
' end Police\n' +
|
||||
'else if sufficient funds\n' +
|
||||
' ATM -> Bank: Withdraw funds\n' +
|
||||
' repeat until "all requested money\n' +
|
||||
' has been handed over"\n' +
|
||||
' ATM -> Person: Dispense note\n' +
|
||||
' end\n' +
|
||||
'else\n' +
|
||||
' ATM -> Person: Error\n' +
|
||||
'end'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Multiline Text" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title \'My Multiline\n' +
|
||||
'Title\'\n' +
|
||||
'\n' +
|
||||
'note over Foo: \'Also possible\\nwith escapes\'\n' +
|
||||
'\n' +
|
||||
'Foo -> Bar: \'Lines of text\\non this arrow\'\n' +
|
||||
'\n' +
|
||||
'if \'Even multiline\\ninside conditions like this\'\n' +
|
||||
' Foo -> \'Multiline\\nagent\'\n' +
|
||||
'end\n' +
|
||||
'\n' +
|
||||
'state over Foo: \'Newlines here,\\ntoo!\''
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Short-Lived Agents" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'title "Baz doesn\'t live long"\n' +
|
||||
'\n' +
|
||||
'Foo -> Bar\n' +
|
||||
'begin Baz\n' +
|
||||
'Bar -> Baz\n' +
|
||||
'Baz -> Foo\n' +
|
||||
'end Baz\n' +
|
||||
'Foo -> Bar\n' +
|
||||
'\n' +
|
||||
'# Foo and Bar end with black bars\n' +
|
||||
'terminators bar\n' +
|
||||
'# (options are: box, bar, cross, none)'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Alternative Agent Ordering" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'define Baz, Foo\n' +
|
||||
'Foo -> Bar\n' +
|
||||
'Bar -> Baz\n'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
|
||||
it('Renders the "Simultaneous Actions" example without error', () => {
|
||||
const parsed = parser.parse(
|
||||
'begin A, B, C, D\n' +
|
||||
'A -> C\n' +
|
||||
'\n' +
|
||||
'# Define a marker which can be returned to later\n' +
|
||||
'\n' +
|
||||
'some primary process:\n' +
|
||||
'A -> B\n' +
|
||||
'B -> A\n' +
|
||||
'A -> B\n' +
|
||||
'B -> A\n' +
|
||||
'\n' +
|
||||
'# Return to the defined marker\n' +
|
||||
'# (should be interpreted as no-higher-then the marker; may be\n' +
|
||||
'# pushed down to keep relative action ordering consistent)\n' +
|
||||
'\n' +
|
||||
'simultaneously with some primary process:\n' +
|
||||
'C -> D\n' +
|
||||
'D -> C\n' +
|
||||
'end D\n' +
|
||||
'C -> A\n' +
|
||||
'\n' +
|
||||
'# The marker name is optional; using "simultaneously:" with no\n' +
|
||||
'# marker will jump to the top of the entire sequence.'
|
||||
);
|
||||
const sequence = generator.generate(parsed);
|
||||
expect(() => renderer.render(sequence)).not.toThrow();
|
||||
});
|
||||
});
|
|
@ -8,4 +8,5 @@ define([
|
|||
'sequence/Generator_spec',
|
||||
'sequence/Renderer_spec',
|
||||
'sequence/themes/Basic_spec',
|
||||
'sequence/sequence_integration_spec',
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
define(['svg/SVGUtilities'], (svg) => {
|
||||
'use strict';
|
||||
|
||||
// Simplified text block renderer, which assumes all characters render as
|
||||
// 1x1 px squares for repeatable renders in all browsers
|
||||
|
||||
function merge(state, newState) {
|
||||
for(let k in state) {
|
||||
if(state.hasOwnProperty(k)) {
|
||||
if(newState[k] !== null && newState[k] !== undefined) {
|
||||
state[k] = newState[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SVGTextBlock {
|
||||
constructor(container, initialState = {}) {
|
||||
this.container = container;
|
||||
this.state = {
|
||||
attrs: {},
|
||||
text: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.nodes = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
||||
_rebuildNodes(count) {
|
||||
if(count > this.nodes.length) {
|
||||
const attrs = Object.assign({
|
||||
'x': this.state.x,
|
||||
}, this.state.attrs);
|
||||
|
||||
while(this.nodes.length < count) {
|
||||
const element = svg.make('text', attrs);
|
||||
const text = svg.makeText();
|
||||
element.appendChild(text);
|
||||
this.container.appendChild(element);
|
||||
this.nodes.push({element, text});
|
||||
}
|
||||
} else {
|
||||
while(this.nodes.length > count) {
|
||||
const {element} = this.nodes.pop();
|
||||
this.container.removeChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._rebuildNodes(0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
if(!this.state.text) {
|
||||
this._reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = this.state.text.split('\n');
|
||||
this._rebuildNodes(lines.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.nodes.forEach(({text, element}, i) => {
|
||||
text.nodeValue = lines[i];
|
||||
maxWidth = Math.max(maxWidth, lines[i].length);
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
this.nodes.forEach(({element}) => {
|
||||
element.setAttribute('x', this.state.x);
|
||||
});
|
||||
}
|
||||
|
||||
_updateY() {
|
||||
this.nodes.forEach(({element}, i) => {
|
||||
element.setAttribute('y', this.state.y + i);
|
||||
});
|
||||
this.height = this.nodes.length;
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
if(this.nodes.length > 0) {
|
||||
return this.nodes[0].element;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
set(newState) {
|
||||
const oldState = Object.assign({}, this.state);
|
||||
merge(this.state, newState);
|
||||
|
||||
if(this.state.attrs !== oldState.attrs) {
|
||||
this._reset();
|
||||
oldState.text = '';
|
||||
}
|
||||
|
||||
const oldNodes = this.nodes.length;
|
||||
|
||||
if(this.state.text !== oldState.text) {
|
||||
this._renderText();
|
||||
}
|
||||
|
||||
if(this.state.x !== oldState.x) {
|
||||
this._updateX();
|
||||
}
|
||||
|
||||
if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) {
|
||||
this._updateY();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SizeTester {
|
||||
measure(attrs, content) {
|
||||
if(!content) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
let width = 0;
|
||||
lines.forEach((line) => {
|
||||
width = Math.max(width, line.length);
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height: lines.length,
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, content) {
|
||||
if(!content) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return content.split('\n').length;
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
}
|
||||
|
||||
detach() {
|
||||
}
|
||||
}
|
||||
|
||||
SVGTextBlock.SizeTester = SizeTester;
|
||||
|
||||
return SVGTextBlock;
|
||||
});
|
|
@ -43,6 +43,7 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
|
|||
boxLayer,
|
||||
labelLayer,
|
||||
boxRenderer = null,
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
}) {
|
||||
if(!text) {
|
||||
return {width: 0, height: 0, label: null, box: null};
|
||||
|
@ -65,7 +66,7 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
|
|||
break;
|
||||
}
|
||||
|
||||
const label = new SVGTextBlock(labelLayer, {
|
||||
const label = new SVGTextBlockClass(labelLayer, {
|
||||
attrs: labelAttrs,
|
||||
text,
|
||||
x: anchorX,
|
||||
|
|
Loading…
Reference in New Issue