diff --git a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts index 6dbf9c081..060ba6911 100644 --- a/packages/mermaid/src/diagrams/class/classDiagram.spec.ts +++ b/packages/mermaid/src/diagrams/class/classDiagram.spec.ts @@ -4,18 +4,13 @@ import classDb from './classDb.js'; import { vi, describe, it, expect } from 'vitest'; const spyOn = vi.spyOn; -describe('class diagram, ', function () { - describe('when parsing a class diagram', function () { +describe('given a class diagram, ', function () { + describe('when parsing class definition', function () { beforeEach(function () { parser.yy = classDb; }); - it('should handle backquoted class names', function () { - const str = 'classDiagram\n' + 'class `Car`'; - - parser.parse(str); - }); - it.skip('should handle a leading newline axa', function () { + it.skip('should handle a leading newline', function () { const str = '\nclassDiagram\n' + 'class Car'; try { @@ -25,161 +20,20 @@ describe('class diagram, ', function () { expect(true).toBe(false); } }); - it('should handle relation definitions', function () { - const str = - 'classDiagram\n' + - 'Class01 <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class1'; + + it('should handle backquoted class names', function () { + const str = 'classDiagram\n' + 'class `Car`'; parser.parse(str); }); - it('should handle backquoted relation definitions', function () { - const str = - 'classDiagram\n' + - '`Class01` <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class1'; + it('should handle class names with underscore', function () { + const str = 'classDiagram\n' + 'class `A_Car`'; parser.parse(str); }); - it('should handle relation definition of different types and directions', function () { - const str = - 'classDiagram\n' + - 'Class11 <|.. Class12\n' + - 'Class13 --> Class14\n' + - 'Class15 ..> Class16\n' + - 'Class17 ..|> Class18\n' + - 'Class19 <--* Class20'; - - parser.parse(str); - }); - - it('should handle cardinality and labels', function () { - const str = - 'classDiagram\n' + - 'Class01 "1" *-- "many" Class02 : contains\n' + - 'Class03 o-- Class04 : aggregation\n' + - 'Class05 --> "1" Class06'; - - parser.parse(str); - }); - - it('should handle visibility for methods and members', function () { - const str = - 'classDiagram\n' + - 'class TestClass\n' + - 'TestClass : -int privateMember\n' + - 'TestClass : +int publicMember\n' + - 'TestClass : #int protectedMember\n' + - 'TestClass : -privateMethod()\n' + - 'TestClass : +publicMethod()\n' + - 'TestClass : #protectedMethod()\n'; - - parser.parse(str); - }); - - it('should handle generic class', function () { - const str = - 'classDiagram\n' + - 'class Car~T~\n' + - 'Driver -- Car : drives >\n' + - 'Car *-- Wheel : have 4 >\n' + - 'Car -- Person : < owns'; - - parser.parse(str); - }); - - it('should handle generic class with a literal name', function () { - const str = - 'classDiagram\n' + - 'class `Car`~T~\n' + - 'Driver -- `Car` : drives >\n' + - '`Car` *-- Wheel : have 4 >\n' + - '`Car` -- Person : < owns'; - - parser.parse(str); - }); - - it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); - }); - - it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Dummy_Class {\n'; - let testPassed = false; - try { - parser.parse(str); - } catch (error) { - testPassed = true; - } - expect(testPassed).toBe(true); - }); - - it('should handle generic class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Dummy_Class~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - - parser.parse(str); - }); - - it('should handle generic class with brackets and a literal name', function () { - const str = - 'classDiagram\n' + - 'class `Dummy_Class`~T~ {\n' + - 'String data\n' + - ' void methods()\n' + - '}\n' + - '\n' + - 'class Flight {\n' + - ' flightNumber : Integer\n' + - ' departureTime : Date\n' + - '}'; - - parser.parse(str); - }); - - it('should handle class definitions', function () { + it('should handle basic class definitions', function () { const str = 'classDiagram\n' + 'class Car\n' + @@ -190,35 +44,23 @@ describe('class diagram, ', function () { parser.parse(str); }); - it('should handle cssClass shorthand with members', () => { - parser.parse(`classDiagram-v2 - class Class10:::exClass2 { - int[] id - List~int~ ids - test(List~int~ ids) List~bool~ - testArray() bool[] - }`); + it('should handle member definitions in brackets', function () { + const str = 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}'; - expect(classDb.getClass('Class10')).toMatchInlineSnapshot(` - { - "annotations": [], - "cssClasses": [ - "exClass2", - ], - "domId": "classId-Class10-27", - "id": "Class10", - "label": "Class10", - "members": [ - "int[] id", - "List~int~ ids", - ], - "methods": [ - "test(List~int~ ids) List~bool~", - "testArray() bool[]", - ], - "type": "", - } - `); + parser.parse(str); + }); + + it('should handle method declaration in brackets', function () { + const str = 'classDiagram\n' + 'class Car{\n' + '+size()\n' + '}'; + + parser.parse(str); + }); + + it('should handle properties in brackets, and some outside', function () { + const str = + 'classDiagram\n' + 'class Car{\n' + '+int wheels\n' + '}\n' + 'Car : +ArrayList size()\n'; + + parser.parse(str); }); it('should handle method statements', function () { @@ -232,6 +74,20 @@ describe('class diagram, ', function () { parser.parse(str); }); + it('should handle visibility for methods and members', function () { + const str = + 'classDiagram\n' + + 'class actual\n' + + 'actual : -int privateMember\n' + + 'actual : +int publicMember\n' + + 'actual : #int protectedMember\n' + + 'actual : -privateMethod()\n' + + 'actual : +publicMethod()\n' + + 'actual : #protectedMethod()\n'; + + parser.parse(str); + }); + it('should handle parsing of method statements grouped by brackets', function () { const str = 'classDiagram\n' + @@ -311,467 +167,6 @@ describe('class diagram, ', function () { parser.parse(str); }); - it('should handle a comment', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}'; - - parser.parse(str); - }); - - it('should handle comments at the start', function () { - const str = `%% Comment - classDiagram - class Class1 { - int : test - string : foo - test() - foo() - }`; - parser.parse(str); - }); - - it('should handle comments at the end', function () { - const str = `classDiagram -class Class1 { -int : test -string : foo -test() -foo() - -} -%% Comment -`; - - parser.parse(str); - }); - - it('should handle comments at the end no trailing newline', function () { - const str = `classDiagram -class Class1 { -int : test -string : foo -test() -foo() -} -%% Comment`; - - parser.parse(str); - }); - - it('should handle a comment with multiple line feeds', function () { - const str = `classDiagram - - -%% Comment - -class Class1 { -int : test -string : foo -test() -foo() -}`; - - parser.parse(str); - }); - - it('should handle a comment with mermaid class diagram code in them', function () { - const str = `classDiagram -%% Comment Class01 <|-- Class02 -class Class1 { -int : test -string : foo -test() -foo() -}`; - - parser.parse(str); - }); - - it('should handle a comment inside brackets', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}'; - - parser.parse(str); - }); - - it('should handle click statement with link', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'link Class01 "google.com" '; - - parser.parse(str); - }); - - it('should handle click statement with click and href link', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'click Class01 href "google.com" '; - - parser.parse(str); - }); - - it('should handle click statement with link and tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'link Class01 "google.com" "A Tooltip" '; - - parser.parse(str); - }); - - it('should handle click statement with click and href link and tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'click Class01 href "google.com" "A Tooltip" '; - - parser.parse(str); - }); - - it('should handle click statement with callback', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'callback Class01 "functionCall" '; - - parser.parse(str); - }); - - it('should handle click statement with click and call callback', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'click Class01 call functionCall() '; - - parser.parse(str); - }); - - it('should handle click statement with callback and tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'callback Class01 "functionCall" "A Tooltip" '; - - parser.parse(str); - }); - - it('should handle click statement with click and call callback and tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '%% Comment Class01 <|-- Class02\n' + - 'int : test\n' + - 'string : foo\n' + - 'test()\n' + - 'foo()\n' + - '}\n' + - 'click Class01 call functionCall() "A Tooltip" '; - - parser.parse(str); - }); - - it('should handle dashed relation definition of different types and directions', function () { - const str = - 'classDiagram\n' + - 'Class11 <|.. Class12\n' + - 'Class13 <.. Class14\n' + - 'Class15 ..|> Class16\n' + - 'Class17 ..> Class18\n' + - 'Class19 .. Class20'; - parser.parse(str); - }); - - it('should handle generic types in members', function () { - const str = - 'classDiagram\n' + - 'class Car~T~\n' + - 'Car : -List~Wheel~ wheels\n' + - 'Car : +setWheels(List~Wheel~ wheels)\n' + - 'Car : +getWheels() List~Wheel~'; - - parser.parse(str); - }); - - it('should handle generic types in members in class with brackets', function () { - const str = - 'classDiagram\n' + - 'class Car {\n' + - 'List~Wheel~ wheels\n' + - 'setWheels(List~Wheel~ wheels)\n' + - '+getWheels() List~Wheel~\n' + - '}'; - - parser.parse(str); - }); - - it('should handle "note for"', function () { - const str = 'classDiagram\n' + 'Class11 <|.. Class12\n' + 'note for Class11 "test"\n'; - parser.parse(str); - }); - - it('should handle "note"', function () { - const str = 'classDiagram\n' + 'note "test"\n'; - parser.parse(str); - }); - }); - - describe('when fetching data from a classDiagram it', function () { - beforeEach(function () { - parser.yy = classDb; - parser.yy.clear(); - }); - it('should handle relation definitions EXTENSION', function () { - const str = 'classDiagram\n' + 'Class01 <|-- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle accTitle and accDescr', function () { - const str = `classDiagram - accTitle: My Title - accDescr: My Description - - Class01 <|-- Class02 - `; - - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('My Title'); - expect(parser.yy.getAccDescription()).toBe('My Description'); - }); - it('should handle accTitle and multiline accDescr', function () { - const str = `classDiagram - accTitle: My Title - accDescr { - This is mu multi - line description - } - - Class01 <|-- Class02 - `; - - parser.parse(str); - expect(parser.yy.getAccTitle()).toBe('My Title'); - expect(parser.yy.getAccDescription()).toBe('This is mu multi\nline description'); - }); - - it('should handle relation definitions AGGREGATION and dotted line', function () { - const str = 'classDiagram\n' + 'Class01 o.. Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); - }); - - it('should handle relation definitions COMPOSITION on both sides', function () { - const str = 'classDiagram\n' + 'Class01 *--* Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION); - expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle relation definitions no types', function () { - const str = 'classDiagram\n' + 'Class01 -- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe('none'); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle relation definitions with type only on right side', function () { - const str = 'classDiagram\n' + 'Class01 --|> Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe('none'); - expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle multiple classes and relation definitions', function () { - const str = - 'classDiagram\n' + - 'Class01 <|-- Class02\n' + - 'Class03 *-- Class04\n' + - 'Class05 o-- Class06\n' + - 'Class07 .. Class08\n' + - 'Class09 -- Class10'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class10').id).toBe('Class10'); - - expect(relations.length).toBe(5); - - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - expect(relations[3].relation.type1).toBe('none'); - expect(relations[3].relation.type2).toBe('none'); - expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); - }); - - it('should handle generic class with relation definitions', function () { - const str = 'classDiagram\n' + 'Class01~T~ <|-- Class02'; - - parser.parse(str); - - const relations = parser.yy.getRelations(); - - expect(parser.yy.getClass('Class01').id).toBe('Class01'); - expect(parser.yy.getClass('Class01').type).toBe('T'); - expect(parser.yy.getClass('Class02').id).toBe('Class02'); - expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); - expect(relations[0].relation.type2).toBe('none'); - expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); - }); - - it('should handle class annotations', function () { - const str = 'classDiagram\n' + 'class Class1\n' + '<> Class1'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(1); - expect(testClass.members.length).toBe(0); - expect(testClass.methods.length).toBe(0); - expect(testClass.annotations[0]).toBe('interface'); - }); - - it('should handle class annotations with members and methods', function () { - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : int test\n' + - 'Class1 : test()\n' + - '<> Class1'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(1); - expect(testClass.members.length).toBe(1); - expect(testClass.methods.length).toBe(1); - expect(testClass.annotations[0]).toBe('interface'); - }); - - it('should handle class annotations in brackets', function () { - const str = 'classDiagram\n' + 'class Class1 {\n' + '<>\n' + '}'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(1); - expect(testClass.members.length).toBe(0); - expect(testClass.methods.length).toBe(0); - expect(testClass.annotations[0]).toBe('interface'); - }); - - it('should handle class annotations in brackets with members and methods', function () { - const str = - 'classDiagram\n' + - 'class Class1 {\n' + - '<>\n' + - 'int : test\n' + - 'test()\n' + - '}'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(1); - expect(testClass.members.length).toBe(1); - expect(testClass.methods.length).toBe(1); - expect(testClass.annotations[0]).toBe('interface'); - }); - it('should add bracket members in right order', function () { const str = 'classDiagram\n' + @@ -783,206 +178,13 @@ foo() '}'; parser.parse(str); - const testClass = parser.yy.getClass('Class1'); - expect(testClass.members.length).toBe(2); - expect(testClass.methods.length).toBe(2); - expect(testClass.members[0]).toBe('int : test'); - expect(testClass.members[1]).toBe('string : foo'); - expect(testClass.methods[0]).toBe('test()'); - expect(testClass.methods[1]).toBe('foo()'); - }); - - it('should handle abstract methods', function () { - const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(0); - expect(testClass.members.length).toBe(0); - expect(testClass.methods.length).toBe(1); - expect(testClass.methods[0]).toBe('someMethod()*'); - }); - - it('should handle static methods', function () { - const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.annotations.length).toBe(0); - expect(testClass.members.length).toBe(0); - expect(testClass.methods.length).toBe(1); - expect(testClass.methods[0]).toBe('someMethod()$'); - }); - - it('should associate link and css appropriately', function () { - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'link Class1 "google.com"'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.link).toBe('google.com'); - expect(testClass.cssClasses.length).toBe(1); - expect(testClass.cssClasses[0]).toBe('clickable'); - }); - - it('should associate click and href link and css appropriately', function () { - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 href "google.com"'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.link).toBe('google.com'); - expect(testClass.cssClasses.length).toBe(1); - expect(testClass.cssClasses[0]).toBe('clickable'); - }); - - it('should associate link with tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'link Class1 "google.com" "A tooltip"'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.link).toBe('google.com'); - expect(testClass.tooltip).toBe('A tooltip'); - expect(testClass.cssClasses.length).toBe(1); - expect(testClass.cssClasses[0]).toBe('clickable'); - }); - - it('should associate click and href link with tooltip', function () { - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 href "google.com" "A tooltip"'; - parser.parse(str); - - const testClass = parser.yy.getClass('Class1'); - expect(testClass.link).toBe('google.com'); - expect(testClass.tooltip).toBe('A tooltip'); - expect(testClass.cssClasses.length).toBe(1); - expect(testClass.cssClasses[0]).toBe('clickable'); - }); - - it('should associate click and href link with tooltip and target appropriately', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 href "google.com" "A tooltip" _self'; - parser.parse(str); - - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); - }); - - it('should associate click and href link appropriately', function () { - spyOn(classDb, 'setLink'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 href "google.com"'; - parser.parse(str); - - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); - }); - - it('should associate click and href link with target appropriately', function () { - spyOn(classDb, 'setLink'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 href "google.com" _self'; - parser.parse(str); - - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - }); - - it('should associate link appropriately', function () { - spyOn(classDb, 'setLink'); - spyOn(classDb, 'setTooltip'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'link Class1 "google.com" "A tooltip" _self'; - parser.parse(str); - - expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); - }); - - it('should associate callback appropriately', function () { - spyOn(classDb, 'setClickEvent'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'callback Class1 "functionCall"'; - parser.parse(str); - - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); - }); - - it('should associate click and call callback appropriately', function () { - spyOn(classDb, 'setClickEvent'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 call functionCall()'; - parser.parse(str); - - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); - }); - - it('should associate callback appropriately with an arbitrary number of args', function () { - spyOn(classDb, 'setClickEvent'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 call functionCall("test0", test1, test2)'; - parser.parse(str); - - expect(classDb.setClickEvent).toHaveBeenCalledWith( - 'Class1', - 'functionCall', - '"test0", test1, test2' - ); - }); - - it('should associate callback with tooltip', function () { - spyOn(classDb, 'setClickEvent'); - spyOn(classDb, 'setTooltip'); - const str = - 'classDiagram\n' + - 'class Class1\n' + - 'Class1 : someMethod()\n' + - 'click Class1 call functionCall() "A tooltip"'; - parser.parse(str); - - expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); - expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); - }); - }); - - describe('when parsing classDiagram with text labels', () => { - beforeEach(function () { - parser.yy = classDb; - parser.yy.clear(); + const actual = parser.yy.getClass('Class1'); + expect(actual.members.length).toBe(2); + expect(actual.methods.length).toBe(2); + expect(actual.members[0]).toBe('int : test'); + expect(actual.members[1]).toBe('string : foo'); + expect(actual.methods[0]).toBe('test()'); + expect(actual.methods[1]).toBe('foo()'); }); it('should parse a class with a text label', () => { @@ -1053,7 +255,6 @@ C1 --> C2 const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.cssClasses.length).toBe(1); expect(c1.members[0]).toBe('+member1'); expect(c1.cssClasses[0]).toBe('styleClass'); }); @@ -1069,7 +270,6 @@ cssClass "C1" styleClass const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.cssClasses.length).toBe(1); expect(c1.members[0]).toBe('+member1'); expect(c1.cssClasses[0]).toBe('styleClass'); }); @@ -1086,12 +286,10 @@ cssClass "C1,C2" styleClass const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.cssClasses.length).toBe(1); expect(c1.cssClasses[0]).toBe('styleClass'); const c2 = classDb.getClass('C2'); expect(c2.label).toBe('Long long long long long long long long long long label'); - expect(c2.cssClasses.length).toBe(1); expect(c2.cssClasses[0]).toBe('styleClass'); }); @@ -1106,12 +304,10 @@ C1 --> C2 const c1 = classDb.getClass('C1'); expect(c1.label).toBe('Class 1 with text label'); - expect(c1.cssClasses.length).toBe(1); expect(c1.cssClasses[0]).toBe('styleClass1'); const c2 = classDb.getClass('C2'); expect(c2.label).toBe('Class 2 !@#$%^&*() label'); - expect(c2.cssClasses.length).toBe(1); expect(c2.cssClasses[0]).toBe('styleClass2'); }); @@ -1164,5 +360,630 @@ class C13["With Città foreign language"] expect(classDb.getClass('C12').label).toBe('With ~!@#$%^&*()_+=-/?'); expect(classDb.getClass('C13').label).toBe('With Città foreign language'); }); + + it('should handle "note for"', function () { + const str = 'classDiagram\n' + 'Class11 <|.. Class12\n' + 'note for Class11 "test"\n'; + parser.parse(str); + }); + + it('should handle "note"', function () { + const str = 'classDiagram\n' + 'note "test"\n'; + parser.parse(str); + }); + + it('should handle accTitle and accDescr', function () { + const str = `classDiagram + accTitle: My Title + accDescr: My Description + + Class1 <|-- Class02 + `; + + parser.parse(str); + expect(parser.yy.getAccTitle()).toBe('My Title'); + expect(parser.yy.getAccDescription()).toBe('My Description'); + }); + + it('should handle accTitle and multiline accDescr', function () { + const str = `classDiagram + accTitle: My Title + accDescr { + This is mu multi + line description + } + + Class1 <|-- Class02 + `; + + parser.parse(str); + expect(parser.yy.getAccTitle()).toBe('My Title'); + expect(parser.yy.getAccDescription()).toBe('This is mu multi\nline description'); + }); + }); + + describe('when parsing method definition', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle abstract methods', function () { + const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()*'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(0); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(1); + expect(actual.methods[0]).toBe('someMethod()*'); + }); + + it('should handle static methods', function () { + const str = 'classDiagram\n' + 'class Class1\n' + 'Class1 : someMethod()$'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(0); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(1); + expect(actual.methods[0]).toBe('someMethod()$'); + }); + + it('should handle generic types in members', function () { + const str = + 'classDiagram\n' + + 'class Car~T~\n' + + 'Car : -List~Wheel~ wheels\n' + + 'Car : +setWheels(List~Wheel~ wheels)\n' + + 'Car : +getWheels() List~Wheel~'; + + parser.parse(str); + }); + + it('should handle generic types in members in class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Car {\n' + + 'List~Wheel~ wheels\n' + + 'setWheels(List~Wheel~ wheels)\n' + + '+getWheels() List~Wheel~\n' + + '}'; + + parser.parse(str); + }); + }); + + describe('when parsing generics', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle generic class', function () { + const str = + 'classDiagram\n' + + 'class Car~T~\n' + + 'Driver -- Car : drives >\n' + + 'Car *-- Wheel : have 4 >\n' + + 'Car -- Person : < owns'; + + parser.parse(str); + }); + + it('should handle generic class with a literal name', function () { + const str = + 'classDiagram\n' + + 'class `Car`~T~\n' + + 'Driver -- `Car` : drives >\n' + + '`Car` *-- Wheel : have 4 >\n' + + '`Car` -- Person : < owns'; + + parser.parse(str); + }); + + it('should handle generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + + parser.parse(str); + }); + + it('should handle generic class with brackets and a literal name', function () { + const str = + 'classDiagram\n' + + 'class `Dummy_Class`~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + + parser.parse(str); + }); + + it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n' + + 'class Flight {\n' + + ' flightNumber : Integer\n' + + ' departureTime : Date\n' + + '}'; + let testPassed = false; + try { + parser.parse(str); + } catch (error) { + testPassed = true; + } + expect(testPassed).toBe(true); + }); + + it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () { + const str = + 'classDiagram\n' + + 'class Dummy_Class~T~ {\n' + + 'String data\n' + + ' void methods()\n' + + '}\n' + + '\n' + + 'class Dummy_Class {\n'; + let testPassed = false; + try { + parser.parse(str); + } catch (error) { + testPassed = true; + } + expect(testPassed).toBe(true); + }); + }); + + describe('when parsing comments', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle comments at the start', function () { + const str = `%% Comment + classDiagram + class Class1 { + int : test + string : foo + test() + foo() + }`; + parser.parse(str); + }); + + it('should handle comments at the end', function () { + const str = `classDiagram +class Class1 { +int : test +string : foo +test() +foo() + +} +%% Comment +`; + + parser.parse(str); + }); + + it('should handle comments at the end no trailing newline', function () { + const str = `classDiagram +class Class1 { +int : test +string : foo +test() +foo() +} +%% Comment`; + + parser.parse(str); + }); + + it('should handle a comment with multiple line feeds', function () { + const str = `classDiagram + + +%% Comment + +class Class1 { +int : test +string : foo +test() +foo() +}`; + + parser.parse(str); + }); + + it('should handle a comment with mermaid class diagram code in them', function () { + const str = `classDiagram +%% Comment Class1 <|-- Class02 +class Class1 { +int : test +string : foo +test() +foo() +}`; + + parser.parse(str); + }); + + it('should handle a comment inside brackets', function () { + const str = + 'classDiagram\n' + + 'class Class1 {\n' + + '%% Comment Class1 <|-- Class02\n' + + 'int : test\n' + + 'string : foo\n' + + 'test()\n' + + 'foo()\n' + + '}'; + + parser.parse(str); + }); + }); + + describe('when parsing relationships', function () { + beforeEach(function () { + parser.yy = classDb; + }); + + it('should handle relation definitions', function () { + const str = + 'classDiagram\n' + + 'Class1 <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class1'; + + parser.parse(str); + }); + + it('should handle backquoted relation definitions', function () { + const str = + 'classDiagram\n' + + '`Class1` <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class1'; + + parser.parse(str); + }); + + it('should handle relation definitions EXTENSION', function () { + const str = 'classDiagram\n' + 'Class1 <|-- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relation definition of different types and directions', function () { + const str = + 'classDiagram\n' + + 'Class11 <|.. Class12\n' + + 'Class13 --> Class14\n' + + 'Class15 ..> Class16\n' + + 'Class17 ..|> Class18\n' + + 'Class19 <--* Class20'; + + parser.parse(str); + }); + + it('should handle cardinality and labels', function () { + const str = + 'classDiagram\n' + + 'Class1 "1" *-- "many" Class02 : contains\n' + + 'Class03 o-- Class04 : aggregation\n' + + 'Class05 --> "1" Class06'; + + parser.parse(str); + }); + + it('should handle dashed relation definition of different types and directions', function () { + const str = + 'classDiagram\n' + + 'Class11 <|.. Class12\n' + + 'Class13 <.. Class14\n' + + 'Class15 ..|> Class16\n' + + 'Class17 ..> Class18\n' + + 'Class19 .. Class20'; + parser.parse(str); + }); + + it('should handle relation definitions AGGREGATION and dotted line', function () { + const str = 'classDiagram\n' + 'Class1 o.. Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe(classDb.relationType.AGGREGATION); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); + }); + + it('should handle relation definitions COMPOSITION on both sides', function () { + const str = 'classDiagram\n' + 'Class1 *--* Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe(classDb.relationType.COMPOSITION); + expect(relations[0].relation.type2).toBe(classDb.relationType.COMPOSITION); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relation definitions no types', function () { + const str = 'classDiagram\n' + 'Class1 -- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe('none'); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle relation definitions with type only on right side', function () { + const str = 'classDiagram\n' + 'Class1 --|> Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe('none'); + expect(relations[0].relation.type2).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + + it('should handle multiple classes and relation definitions', function () { + const str = + 'classDiagram\n' + + 'Class1 <|-- Class02\n' + + 'Class03 *-- Class04\n' + + 'Class05 o-- Class06\n' + + 'Class07 .. Class08\n' + + 'Class09 -- Class10'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class10').id).toBe('Class10'); + + expect(relations.length).toBe(5); + + expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + expect(relations[3].relation.type1).toBe('none'); + expect(relations[3].relation.type2).toBe('none'); + expect(relations[3].relation.lineType).toBe(classDb.lineType.DOTTED_LINE); + }); + + it('should handle generic class with relation definitions', function () { + const str = 'classDiagram\n' + 'Class1~T~ <|-- Class02'; + + parser.parse(str); + + const relations = parser.yy.getRelations(); + + expect(parser.yy.getClass('Class1').id).toBe('Class1'); + expect(parser.yy.getClass('Class1').type).toBe('T'); + expect(parser.yy.getClass('Class02').id).toBe('Class02'); + expect(relations[0].relation.type1).toBe(classDb.relationType.EXTENSION); + expect(relations[0].relation.type2).toBe('none'); + expect(relations[0].relation.lineType).toBe(classDb.lineType.LINE); + }); + }); + + describe('when parsing click statements', function () { + beforeEach(function () { + parser.yy = classDb; + }); + it('should handle href link', function () { + spyOn(classDb, 'setLink'); + const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 href "google.com" '; + + parser.parse(str); + + expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); + + const actual = parser.yy.getClass('Class1'); + expect(actual.link).toBe('google.com'); + expect(actual.cssClasses[0]).toBe('clickable'); + }); + + it('should handle href link with tooltip', function () { + spyOn(classDb, 'setLink'); + spyOn(classDb, 'setTooltip'); + const str = + 'classDiagram\n' + 'class Class1 \n' + 'click Class1 href "google.com" "A Tooltip" '; + + parser.parse(str); + + expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com'); + + const actual = parser.yy.getClass('Class1'); + expect(actual.link).toBe('google.com'); + expect(actual.tooltip).toBe('A Tooltip'); + expect(actual.cssClasses[0]).toBe('clickable'); + }); + + it('should handle href link with tooltip and target', function () { + spyOn(classDb, 'setLink'); + spyOn(classDb, 'setTooltip'); + const str = + 'classDiagram\n' + + 'class Class1\n' + + 'Class1 : someMethod()\n' + + 'click Class1 href "google.com" "A tooltip" _self'; + parser.parse(str); + + expect(classDb.setLink).toHaveBeenCalledWith('Class1', 'google.com', '_self'); + expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A tooltip'); + + const actual = parser.yy.getClass('Class1'); + expect(actual.link).toBe('google.com'); + expect(actual.tooltip).toBe('A tooltip'); + expect(actual.cssClasses[0]).toBe('clickable'); + }); + + it('should handle function call', function () { + spyOn(classDb, 'setClickEvent'); + + const str = 'classDiagram\n' + 'class Class1 \n' + 'click Class1 call functionCall() '; + + parser.parse(str); + + expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + }); + + it('should handle function call with tooltip', function () { + spyOn(classDb, 'setClickEvent'); + spyOn(classDb, 'setTooltip'); + + const str = + 'classDiagram\n' + 'class Class1 \n' + 'click Class1 call functionCall() "A Tooltip" '; + + parser.parse(str); + + expect(classDb.setClickEvent).toHaveBeenCalledWith('Class1', 'functionCall'); + expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); + }); + + it('should handle function call with an arbitrary number of args', function () { + spyOn(classDb, 'setClickEvent'); + const str = + 'classDiagram\n' + + 'class Class1\n' + + 'Class1 : someMethod()\n' + + 'click Class1 call functionCall(test, test1, test2)'; + parser.parse(str); + + expect(classDb.setClickEvent).toHaveBeenCalledWith( + 'Class1', + 'functionCall', + 'test, test1, test2' + ); + }); + + it('should handle function call with an arbitrary number of args and tooltip', function () { + spyOn(classDb, 'setClickEvent'); + spyOn(classDb, 'setTooltip'); + const str = + 'classDiagram\n' + + 'class Class1\n' + + 'Class1 : someMethod()\n' + + 'click Class1 call functionCall("test0", test1, test2) "A Tooltip"'; + parser.parse(str); + + expect(classDb.setClickEvent).toHaveBeenCalledWith( + 'Class1', + 'functionCall', + '"test0", test1, test2' + ); + expect(classDb.setTooltip).toHaveBeenCalledWith('Class1', 'A Tooltip'); + }); + }); + + describe('when parsing annotations', function () { + beforeEach(function () { + parser.yy = classDb; + parser.yy.clear(); + }); + + it('should handle class annotations', function () { + const str = 'classDiagram\n' + 'class Class1\n' + '<> Class1'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(1); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(0); + expect(actual.annotations[0]).toBe('interface'); + }); + + it('should handle class annotations with members and methods', function () { + const str = + 'classDiagram\n' + + 'class Class1\n' + + 'Class1 : int test\n' + + 'Class1 : test()\n' + + '<> Class1'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(1); + expect(actual.members.length).toBe(1); + expect(actual.methods.length).toBe(1); + expect(actual.annotations[0]).toBe('interface'); + }); + + it('should handle class annotations in brackets', function () { + const str = 'classDiagram\n' + 'class Class1 {\n' + '<>\n' + '}'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(1); + expect(actual.members.length).toBe(0); + expect(actual.methods.length).toBe(0); + expect(actual.annotations[0]).toBe('interface'); + }); + + it('should handle class annotations in brackets with members and methods', function () { + const str = + 'classDiagram\n' + + 'class Class1 {\n' + + '<>\n' + + 'int : test\n' + + 'test()\n' + + '}'; + parser.parse(str); + + const actual = parser.yy.getClass('Class1'); + expect(actual.annotations.length).toBe(1); + expect(actual.members.length).toBe(1); + expect(actual.methods.length).toBe(1); + expect(actual.annotations[0]).toBe('interface'); + }); }); }); diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 0c9ad2f2a..4354c54be 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -200,7 +200,7 @@ Function arguments are optional: 'call ()' simply executes 'callb start : mermaidDoc - | statments + | statements | direction | directive start ;