diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts index 4517ff6229..20d86d9b59 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts @@ -35,6 +35,7 @@ describe('diagram-orchestration', () => { { text: 'gitGraph', expected: 'gitGraph' }, { text: 'stateDiagram', expected: 'state' }, { text: 'stateDiagram-v2', expected: 'stateDiagram' }, + { text: 'usecase-beta', expected: 'usecase' }, ])( 'should $text be detected as $expected', ({ text, expected }: { text: string; expected: string }) => { diff --git a/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts b/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts index 078063b1d2..428166fb78 100644 --- a/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts +++ b/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts @@ -1,10 +1,12 @@ // @ts-ignore: jison doesn't export types import usecase from './usecase.jison'; -import db, { UsecaseLink, UsecaseNode } from '../usecaseDB.js'; -import { cleanupComments } from '../../../diagram-api/comments.js'; +import db, { type UsecaseDB, UsecaseLink, UsecaseNode } from '../usecaseDB.js'; import { prepareTextForParsing } from '../usecaseUtils.js'; -import * as fs from 'fs'; -import * as path from 'path'; +import { setConfig } from '../../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); describe('Usecase diagram', function () { describe('when parsing a use case it', function () { @@ -28,16 +30,21 @@ describe('Usecase diagram', function () { const relationships = usecase.yy.getRelationships(); expect(relationships).toStrictEqual([ - new UsecaseLink(new UsecaseNode('User'), new UsecaseNode('(Start)'), '->', ''), new UsecaseLink( - new UsecaseNode('User'), - new UsecaseNode('(Use the application)'), + new UsecaseNode('User', 'actor'), + new UsecaseNode('(Start)', 'usecase'), + '->', + '' + ), + new UsecaseLink( + new UsecaseNode('User', 'actor'), + new UsecaseNode('(Use the application)', 'usecase'), '-->', '' ), new UsecaseLink( - new UsecaseNode('(Use the application)'), - new UsecaseNode('(Another use case)'), + new UsecaseNode('(Use the application)', 'usecase'), + new UsecaseNode('(Another use case)', 'usecase'), '-->', '' ), @@ -119,6 +126,12 @@ describe('Usecase diagram', function () { const result = usecase.parse(prepareTextForParsing(input)); expect(result).toBeTruthy(); expect(usecase.yy.getDiagramTitle()).toEqual('Arrows in Use Case diagrams'); + + const db = usecase.yy as UsecaseDB; + expect( + db.getRelationships().some((rel) => rel.source.id == 'Admin' && rel.target.id == '(Login)') + ).toBeTruthy(); + // console.log(usecase.yy.getRelationships()); // console.log(usecase.yy.getSystemBoundaries()); }); diff --git a/packages/mermaid/src/diagrams/usecase/usecaseDB.ts b/packages/mermaid/src/diagrams/usecase/usecaseDB.ts index 2ffc1eed10..8aa47de31c 100644 --- a/packages/mermaid/src/diagrams/usecase/usecaseDB.ts +++ b/packages/mermaid/src/diagrams/usecase/usecaseDB.ts @@ -13,6 +13,14 @@ import { clear as commonClear, } from '../common/commonDb.js'; +function isUseCaseLabel(label: string | undefined) { + if (!label) { + return false; + } + label = label.trim(); + return label.startsWith('(') && label.endsWith(')'); +} + export class UsecaseDB { private aliases = new Map(); private links: UsecaseLink[] = []; @@ -41,20 +49,20 @@ export class UsecaseDB { addParticipants(participant: { service: string } | { actor: string }) { if ('actor' in participant && !this.nodes.some((node) => node.id === participant.actor)) { - this.nodes.push(new UsecaseNode(participant.actor)); + this.nodes.push(new UsecaseNode(participant.actor, 'actor')); } else if ( 'service' in participant && !this.nodes.some((node) => node.id === participant.service) ) { - this.nodes.push(new UsecaseNode(participant.service)); + this.nodes.push(new UsecaseNode(participant.service, 'service')); } } addRelationship(source: string, target: string, token: string): void { source = common.sanitizeText(source, getConfig()); target = common.sanitizeText(target, getConfig()); - const sourceNode = this.getNode(source); - const targetNode = this.getNode(target); + const sourceNode = this.getNode(source, isUseCaseLabel(source) ? 'usecase' : 'actor'); + const targetNode = this.getNode(target, isUseCaseLabel(target) ? 'usecase' : 'service'); const label = (/--(.+?)(-->|->)/.exec(token)?.[1] ?? '').trim(); const arrow = token.includes('-->') ? '-->' : '->'; this.links.push(new UsecaseLink(sourceNode, targetNode, arrow, label)); @@ -71,7 +79,7 @@ export class UsecaseDB { } getActors() { - return this.links.map((link) => link.source.id).filter((source) => !source.startsWith('(')); + return this.links.map((link) => link.source.id).filter((source) => !isUseCaseLabel(source)); } getConfig() { @@ -133,7 +141,7 @@ export class UsecaseDB { ]; nodes - .filter((node) => node.label?.startsWith('(') && node.label.endsWith(')')) + .filter((node) => isUseCaseLabel(node.label)) .forEach((node) => { node.label = node.label!.slice(1, -1); node.rx = 50; @@ -159,6 +167,16 @@ export class UsecaseDB { return this.links; } + getServices(): string[] { + const services = []; + for (const node of this.nodes) { + if (node.nodeType === 'service') { + services.push(node.id); + } + } + return services; + } + getSystemBoundaries() { return this.systemBoundaries; } @@ -171,9 +189,9 @@ export class UsecaseDB { setAccDescription = setAccDescription; setDiagramTitle = setDiagramTitle; - private getNode(id: string): UsecaseNode { + private getNode(id: string, nodeType: UsecaseNodeType): UsecaseNode { if (!this.nodesMap.has(id)) { - const node = new UsecaseNode(id); + const node = new UsecaseNode(id, nodeType); this.nodesMap.set(id, node); this.nodes.push(node); } @@ -199,9 +217,14 @@ export class UsecaseLink { } export class UsecaseNode { - constructor(public id: string) {} + constructor( + public id: string, + public nodeType: UsecaseNodeType | undefined + ) {} } +type UsecaseNodeType = 'actor' | 'service' | 'usecase'; + type ArrowType = '->' | '-->'; // Export an instance of the class diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.spec.ts new file mode 100644 index 0000000000..9c56b61b8b --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { shapes, isValidShape, type ShapeID, shapesDefs } from './shapes.js'; + +describe('shapes', () => { + it('should have a valid shape handler for each shape', () => { + Object.keys(shapes).forEach((shape) => { + expect(typeof shapes[shape as ShapeID]).toBe('function'); + }); + }); + + it('should return true for valid shape IDs', () => { + const validShapes: ShapeID[] = Object.keys(shapes) as ShapeID[]; + validShapes.forEach((shape) => { + expect(isValidShape(shape)).toBe(true); + }); + }); + + it('should return false for invalid shape IDs', () => { + const invalidShapes = ['invalidShape1', 'invalidShape2', 'invalidShape3']; + invalidShapes.forEach((shape) => { + expect(isValidShape(shape)).toBe(false); + }); + }); + + /* + it('should have unique short names and aliases', () => { + const allNames = new Set(); + shapesDefs.forEach((shape) => { + const names = [shape.shortName, ...(shape.aliases ?? []), ...(shape.internalAliases ?? [])]; + names.forEach((name) => { + expect(allNames.has(name)).toBe(false); + allNames.add(name); + }); + }); + }); + */ + + it('should have a handler for each shape definition', () => { + shapesDefs.forEach((shape) => { + expect(typeof shape.handler).toBe('function'); + }); + }); + + it('should have a semantic name, name, short name, and description for each shape definition', () => { + shapesDefs.forEach((shape) => { + expect(typeof shape.semanticName).toBe('string'); + expect(typeof shape.name).toBe('string'); + expect(typeof shape.shortName).toBe('string'); + expect(typeof shape.description).toBe('string'); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index 7e6f01a9c5..6586fa2228 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -83,7 +83,7 @@ export interface ShapeDefinition { handler: ShapeHandler; } -export const shapesDefs = [ +export const shapesDefs: ShapeDefinition[] = [ { semanticName: 'Actor', name: 'Actor',