From a3fb490153031747ea551f6ea04ef73b8550e65b Mon Sep 17 00:00:00 2001 From: Chui Tey Date: Fri, 20 Dec 2024 23:57:26 +1000 Subject: [PATCH] Fix unit test, add more tests --- packages/mermaid/scripts/docs.spec.ts | 2 +- .../src/diagrams/usecase/parser/usecase.jison | 28 ++++++--- .../diagrams/usecase/parser/usecase.spec.ts | 22 +++++++ .../src/diagrams/usecase/usecase.spec.ts | 2 +- .../mermaid/src/diagrams/usecase/usecaseDB.ts | 62 +++++++++---------- 5 files changed, 74 insertions(+), 42 deletions(-) diff --git a/packages/mermaid/scripts/docs.spec.ts b/packages/mermaid/scripts/docs.spec.ts index b11fee2a07..404d775e2e 100644 --- a/packages/mermaid/scripts/docs.spec.ts +++ b/packages/mermaid/scripts/docs.spec.ts @@ -171,7 +171,7 @@ This Markdown should be kept. expect(buildShapeDoc()).toMatchInlineSnapshot(` "| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | | --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- | - | Actor | Actor | \`actor\` | Actor used in Use Cases | | + | Actor | Actor | \`actor\` | Actor used in Use Cases | \`stickman\` | | Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` | | Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` | | Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` | diff --git a/packages/mermaid/src/diagrams/usecase/parser/usecase.jison b/packages/mermaid/src/diagrams/usecase/parser/usecase.jison index 32ac1d43d3..713a79bb71 100644 --- a/packages/mermaid/src/diagrams/usecase/parser/usecase.jison +++ b/packages/mermaid/src/diagrams/usecase/parser/usecase.jison @@ -8,14 +8,14 @@ %options case-insensitive // Special states for recognizing aliases and notes -%x ALIAS NOTE +%x ALIAS NOTE ID %% \s+ /* skip whitespace */ "usecase-beta" return 'DECLARATION'; -"actor" return 'ACTOR'; -"service" return 'SERVICE'; +"actor" { this.begin('ID'); return 'ACTOR'; } +"service" { this.begin('ID'); return 'SERVICE'; } "systemboundary" return 'SYSTEMBOUNDARY'; "-"\s[^#\n;]+ return 'TASK'; "title"\s[^#\n;]+ return 'TITLE'; @@ -28,6 +28,18 @@ "->" return 'SOLID_ARROW'; "-" return '-'; "end" return 'END'; +(.+)\s+as\s+([^\n]+) { + var matches = yytext.match(/\s*(.+)\s+as\s+([^\n]+)/); + this.popState(); + yytext = [matches[1], matches[2]]; + return 'IDENTIFIER_AS'; +} +([^\n]+) { + var matches = yytext.match(/\s*([^\n]+)/); + this.popState(); + yytext = matches[1]; + return 'IDENTIFIER'; +} \([^)\n]*\)\s+as\s+\([^)\n]*\) return 'USECASE_AS'; \([^)\n]*\) return 'USECASE' "(" return '('; @@ -61,13 +73,15 @@ optional_sections ; participant_definitions - : participant_definitions participant_definition { yy.addParticipants($2); } - | participant_definition + : participant_definitions participant_definition { yy.addParticipant($2); } + | participant_definition { yy.addParticipant($1); } ; participant_definition - : 'SERVICE' IDENTIFIER { $$ = {'service': $2} } - | 'ACTOR' IDENTIFIER { $$ = {'actor' : $2} } + : SERVICE IDENTIFIER { $$ = {'service': $2} } + | ACTOR IDENTIFIER { $$ = {'actor' : $2} } + | SERVICE IDENTIFIER_AS { $$ = {'service': $2[0], 'as': $2[1]} } + | ACTOR IDENTIFIER_AS { $$ = {'actor' : $2[0], 'as': $2[1]} } ; systemboundary_definitions diff --git a/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts b/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts index 428166fb78..4eca7c84a0 100644 --- a/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts +++ b/packages/mermaid/src/diagrams/usecase/parser/usecase.spec.ts @@ -135,5 +135,27 @@ describe('Usecase diagram', function () { // console.log(usecase.yy.getRelationships()); // console.log(usecase.yy.getSystemBoundaries()); }); + + it('should handle aliases', function () { + const input = ` + usecase-beta + actor SU as Student Union + actor University Chancellor + service PMS as Payment Management System + SU -> (Pay Dues) -> PMS + `; + const result = usecase.parse(prepareTextForParsing(input)); + const db = usecase.yy as UsecaseDB; + expect(result).toBeTruthy(); + expect(db.getActors()).toStrictEqual(['SU', 'University Chancellor']); + expect(db.getServices()).toStrictEqual(['PMS']); + expect(db.getData().nodes.find((n) => n.id === 'SU')?.label).toEqual('Student Union'); + expect(db.getData().nodes.find((n) => n.id === 'University Chancellor')?.label).toEqual( + 'University Chancellor' + ); + expect(db.getData().nodes.find((n) => n.id === 'PMS')?.label).toEqual( + 'Payment Management System' + ); + }); }); }); diff --git a/packages/mermaid/src/diagrams/usecase/usecase.spec.ts b/packages/mermaid/src/diagrams/usecase/usecase.spec.ts index feccd63237..df3e0bc60a 100644 --- a/packages/mermaid/src/diagrams/usecase/usecase.spec.ts +++ b/packages/mermaid/src/diagrams/usecase/usecase.spec.ts @@ -6,7 +6,7 @@ describe('Usecase diagram', function () { db.clear(); }); it('should add node when adding participants', function () { - db.addParticipants({ service: 'Authz' }); + db.addParticipant({ service: 'Authz' }); expect(db.getServices()).toStrictEqual(['Authz']); }); it('should add actor when adding relationship', function () { diff --git a/packages/mermaid/src/diagrams/usecase/usecaseDB.ts b/packages/mermaid/src/diagrams/usecase/usecaseDB.ts index 4767a1d67b..5ccf862096 100644 --- a/packages/mermaid/src/diagrams/usecase/usecaseDB.ts +++ b/packages/mermaid/src/diagrams/usecase/usecaseDB.ts @@ -1,7 +1,7 @@ import type { BaseDiagramConfig, MermaidConfig } from '../../config.type.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { log } from '../../logger.js'; + +import { log, setLogLevel } from '../../logger.js'; import type { LayoutData } from '../../mermaid.js'; import type { Edge, Node } from '../../rendering-util/types.js'; import common from '../common/common.js'; @@ -26,7 +26,6 @@ function isUseCaseLabel(label: string | undefined) { export class UsecaseDB { private aliases = new Map(); private links: UsecaseLink[] = []; - private nodes: UsecaseNode[] = []; private nodesMap = new Map(); private systemBoundaries: UsecaseSystemBoundary[] = []; @@ -37,7 +36,6 @@ export class UsecaseDB { clear(): void { this.aliases = new Map(); this.links = []; - this.nodes = []; this.nodesMap = new Map(); this.systemBoundaries = []; commonClear(); @@ -49,14 +47,14 @@ export class UsecaseDB { return source; } - 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, 'actor')); - } else if ( - 'service' in participant && - !this.nodes.some((node) => node.id === participant.service) - ) { - this.nodes.push(new UsecaseNode(participant.service, 'service')); + addParticipant(participant: { service: string; as?: string } | { actor: string; as?: string }) { + setLogLevel('info'); + log.info('addParticipants', participant); + const nodeType = 'actor' in participant ? 'actor' : 'service'; + const id = 'actor' in participant ? participant.actor.trim() : participant.service.trim(); + const _ = this.getOrCreateNode(id, nodeType); + if (participant.as) { + this.addAlias(`${id} as ${participant.as}`); } } @@ -69,8 +67,8 @@ export class UsecaseDB { addRelationship(source: string, target: string, arrowString: string): void { source = common.sanitizeText(source, getConfig()); target = common.sanitizeText(target, getConfig()); - const sourceNode = this.getNode(source, isUseCaseLabel(source) ? 'usecase' : 'actor'); - const targetNode = this.getNode(target, isUseCaseLabel(target) ? 'usecase' : 'service'); + const sourceNode = this.getOrCreateNode(source, isUseCaseLabel(source) ? 'usecase' : 'actor'); + const targetNode = this.getOrCreateNode(target, isUseCaseLabel(target) ? 'usecase' : 'service'); const label = (/--(.+?)(-->|->)/.exec(arrowString)?.[1] ?? '').trim(); const arrow = arrowString.includes('-->') ? '-->' : '->'; this.links.push(new UsecaseLink(sourceNode, targetNode, arrow, label)); @@ -87,7 +85,9 @@ export class UsecaseDB { } getActors() { - return this.links.map((link) => link.source.id).filter((source) => !isUseCaseLabel(source)); + return [...this.nodesMap.values()] + .filter((node) => node.nodeType === 'actor') + .map((node) => node.id); } getConfig() { @@ -125,7 +125,7 @@ export class UsecaseDB { ); const nodes: Node[] = [ - ...this.nodes.map( + ...[...this.nodesMap.values()].map( (node) => ({ ...baseNode, @@ -148,6 +148,8 @@ export class UsecaseDB { ), ]; + // Remove () from use case labels + // and make them rounded. nodes .filter((node) => isUseCaseLabel(node.label)) .forEach((node) => { @@ -176,13 +178,9 @@ export class UsecaseDB { } getServices(): string[] { - const services = []; - for (const node of this.nodes) { - if (node.nodeType === 'service') { - services.push(node.id); - } - } - return services; + return [...this.nodesMap.values()] + .filter((node) => node.nodeType === 'service') + .map((node) => node.id); } getSystemBoundaries() { @@ -190,13 +188,9 @@ export class UsecaseDB { } getUseCases(): string[] { - const useCases = []; - for (const node of this.nodes) { - if (node.nodeType === 'usecase') { - useCases.push(node.id); - } - } - return useCases; + return [...this.nodesMap.values()] + .filter((node) => node.nodeType === 'usecase') + .map((node) => node.id); } getAccDescription = getAccDescription; @@ -207,11 +201,10 @@ export class UsecaseDB { setAccDescription = setAccDescription; setDiagramTitle = setDiagramTitle; - private getNode(id: string, nodeType: UsecaseNodeType): UsecaseNode { + private getOrCreateNode(id: string, nodeType: UsecaseNodeType): UsecaseNode { if (!this.nodesMap.has(id)) { const node = new UsecaseNode(id, nodeType); this.nodesMap.set(id, node); - this.nodes.push(node); } return this.nodesMap.get(id)!; } @@ -234,6 +227,9 @@ export class UsecaseLink { ) {} } +/** + * can be any of actor, service, or usecase + */ export class UsecaseNode { constructor( public id: string, @@ -249,7 +245,7 @@ type ArrowType = '->' | '-->'; const db = new UsecaseDB(); export default { clear: db.clear.bind(db), - addParticipants: db.addParticipants.bind(db), + addParticipant: db.addParticipant.bind(db), addRelationship: db.addRelationship.bind(db), addAlias: db.addAlias.bind(db), getAccDescription,