diff --git a/cypress/integration/rendering/sequencediagram-v2.spec.js b/cypress/integration/rendering/sequencediagram-v2.spec.js index f1c2aafbd..42db4001d 100644 --- a/cypress/integration/rendering/sequencediagram-v2.spec.js +++ b/cypress/integration/rendering/sequencediagram-v2.spec.js @@ -655,5 +655,126 @@ describe('Sequence Diagram Special Cases', () => { expect(svg).to.not.have.attr('style'); }); }); + + describe('Central Connection Rendering Tests', () => { + it('should render central connection circles on actor vertical lines', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Central connection + Bob ()-->> Charlie: Reverse central connection + Charlie ()<<-->>() Alice: Dual central connection`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with different arrow types', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + Alice ()->>() Bob: Solid open arrow + Alice ()-->>() Bob: Dotted open arrow + Alice ()-x() Bob: Solid cross + Alice ()--x() Bob: Dotted cross + Alice ()->() Bob: Solid arrow`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with bidirectional arrows', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + Alice ()<<->>() Bob: Bidirectional solid + Alice ()<<-->>() Bob: Bidirectional dotted`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with activations', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Activate Bob + activate Bob + Bob ()-->> Charlie: Message to Charlie + Bob ()->>() Alice: Response to Alice + deactivate Bob`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections mixed with normal messages', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ->> Bob: Normal message + Bob ()->>() Charlie: Central connection + Charlie -->> Alice: Normal dotted message + Alice ()<<-->>() Bob: Dual central connection + Bob -x Charlie: Normal cross message`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with notes', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Central connection + Note over Alice,Bob: Central connection note + Bob ()-->> Charlie: Reverse central connection + Note right of Charlie: Response note + Charlie ()<<-->>() Alice: Dual central connection`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with loops and alternatives', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + participant Bob + participant Charlie + loop Every minute + Alice ()->>() Bob: Central heartbeat + Bob ()-->> Charlie: Forward heartbeat + end + alt Success + Charlie ()<<-->>() Alice: Success response + else Failure + Charlie ()-x() Alice: Failure response + end`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + + it('should render central connections with different participant types', () => { + imgSnapshotTest( + `sequenceDiagram + participant Alice + actor Bob + participant Charlie@{"type":"boundary"} + participant David@{"type":"control"} + participant Eve@{"type":"entity"} + Alice ()->>() Bob: To actor + Bob ()-->> Charlie: To boundary + Charlie ()->>() David: To control + David ()<<-->>() Eve: To entity + Eve ()-x() Alice: Back to participant`, + { look: 'classic', sequence: { diagramMarginX: 50, diagramMarginY: 10 } } + ); + }); + }); }); }); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 68c1c7a29..c09a92737 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -187,6 +187,254 @@ describe('more than one sequence diagram', () => { }); }); +describe('Central Connection Parsing', () => { + describe('when parsing central connection syntax', () => { + it('should parse actor ()->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + // Find the actual message (type: 'addMessage') + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 0, // SOLID (based on test output) + }); + }); + + it('should parse actor ()-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 1, // DOTTED (based on test output) + }); + }); + + it('should parse actor ->>() actor syntax as CENTRAL_CONNECTION', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnection (no activation for this pattern) + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 59, // CENTRAL_CONNECTION + activate: true, + type: 0, // SOLID (based on actual parsing) + }); + }); + + it('should parse actor ()-->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-->> Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE + activate: false, + type: 1, // DOTTED (based on test output) + }); + }); + + it('should parse actor ()->> actor syntax as CENTRAL_CONNECTION_REVERSE', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()->> Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(2); // addMessage + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE + activate: false, + type: 0, // SOLID (based on test output) + }); + }); + + it('should parse actor ()<<-->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()<<-->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 34, // BIDIRECTIONAL_DOTTED + }); + }); + + it('should parse actor ()<<->>() actor syntax as CENTRAL_CONNECTION_DUAL', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()<<->>() Bob: Hello Bob, how are you? + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); // addMessage + centralConnection + centralConnectionReverse + + const actualMessage = messages.find((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessage).toMatchObject({ + from: 'Alice', + to: 'Bob', + message: 'Hello Bob, how are you?', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL + activate: true, + type: 33, // BIDIRECTIONAL_SOLID + }); + }); + + it('should handle multiple central connection types in one diagram', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + participant Charlie + Alice ()->>() Bob: Message 1 + Bob ()-->> Charlie: Message 2 + Charlie ()<<-->>() Alice: Message 3 + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(8); // 3 addMessages + 5 central connection markers + + // Filter to get only the actual messages + const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessages).toHaveLength(3); + + expect(actualMessages[0]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()->>()) + }); + + expect(actualMessages[1]).toMatchObject({ + from: 'Bob', + to: 'Charlie', + centralConnection: 60, // CENTRAL_CONNECTION_REVERSE (()-->>) + }); + + expect(actualMessages[2]).toMatchObject({ + from: 'Charlie', + to: 'Alice', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()<<-->>()) + }); + }); + + it('should handle central connections with different arrow types', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ()-x() Bob: Cross message + Alice ()--x() Bob: Dotted cross message + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(6); // 2 addMessages + 4 central connection markers + + const actualMessages = messages.filter((msg) => msg.type !== undefined && msg.from && msg.to); + expect(actualMessages).toHaveLength(2); + + expect(actualMessages[0]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()-x()) + type: 3, // SOLID_CROSS + }); + + expect(actualMessages[1]).toMatchObject({ + from: 'Alice', + to: 'Bob', + centralConnection: 61, // CENTRAL_CONNECTION_DUAL (()--x()) + type: 4, // DOTTED_CROSS + }); + }); + + it('should not break existing parsing without central connections', async () => { + const diagram = await Diagram.fromText(` + sequenceDiagram + participant Alice + participant Bob + Alice ->> Bob: Normal message + Bob -->> Alice: Normal dotted message + Alice -x Bob: Normal cross message + `); + + const messages = diagram.db.getMessages(); + expect(messages).toHaveLength(3); + + messages.forEach((msg) => { + expect(msg.centralConnection).toBe(0); // No central connection + }); + + expect(messages[0].type).toBe(0); // SOLID (based on actual parsing) + expect(messages[1].type).toBe(1); // DOTTED (based on actual parsing) + expect(messages[2].type).toBe(3); // SOLID_CROSS + }); + }); +}); + describe('when parsing a sequenceDiagram', function () { let diagram; beforeEach(async function () { diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 86d57757a..6c87ef124 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -292,9 +292,10 @@ const drawCentralConnection = function ( lineStartY: number ) { const actors = diagObj.db.getActors(); - const [fromLeft] = activationBounds(msg.from, actors); - const [toLeft] = activationBounds(msg.to, actors); - const isArrowToRight = fromLeft <= toLeft; + const fromActor = actors.get(msg.from); + const toActor = actors.get(msg.to); + const fromCenter = fromActor.x + fromActor.width / 2; + const toCenter = toActor.x + toActor.width / 2; const g = elem.append('g'); @@ -307,16 +308,20 @@ const drawCentralConnection = function ( .attr('height', 10); }; - if (msg.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION) { - const cx = isArrowToRight ? stopx + 5 : stopx - 8; - drawCircle(cx); - } else if (msg.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE) { - const cx = isArrowToRight ? startx - 5 : stopx + 8; - drawCircle(cx); - } else if (msg.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_DUAL) { - const offset = isArrowToRight ? 5 : -5; - drawCircle(stopx + offset); - drawCircle(startx - offset); + const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = + diagObj.db.LINETYPE; + + switch (msg.centralConnection) { + case CENTRAL_CONNECTION: + drawCircle(toCenter); + break; + case CENTRAL_CONNECTION_REVERSE: + drawCircle(fromCenter); + break; + case CENTRAL_CONNECTION_DUAL: + drawCircle(fromCenter); + drawCircle(toCenter); + break; } }; @@ -471,7 +476,7 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO line.attr('y1', lineStartY); line.attr('x2', stopx); line.attr('y2', lineStartY); - if (msg.centralConnection) { + if (hasCentralConnection(msg, diagObj)) { drawCentralConnection(diagram, msg, msgModel, diagObj, startx, stopx, lineStartY); } } @@ -1600,6 +1605,51 @@ const buildNoteModel = async function (msg, actors, diagObj) { return noteModel; }; +// Central connection positioning constants +const CENTRAL_CONNECTION_BASE_OFFSET = 4; +const CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET = 6; + +/** + * Check if a message has central connection + * @param msg - The message object + * @param diagObj - The diagram object containing LINETYPE constants + * @returns True if the message has any type of central connection + */ +const hasCentralConnection = function (msg, diagObj) { + const { CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL } = + diagObj.db.LINETYPE; + return [CENTRAL_CONNECTION, CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL].includes( + msg.centralConnection + ); +}; + +/** + * Calculate the positioning offset for central connection arrows + * @param msg - The message object + * @param diagObj - The diagram object containing LINETYPE constants + * @param isArrowToRight - Whether the arrow is pointing to the right + * @returns The offset to apply to startx position + */ +const calculateCentralConnectionOffset = function (msg, diagObj, isArrowToRight) { + const { CENTRAL_CONNECTION_REVERSE, CENTRAL_CONNECTION_DUAL, BIDIRECTIONAL_SOLID } = + diagObj.db.LINETYPE; + + let offset = 0; + + if ( + msg.centralConnection === CENTRAL_CONNECTION_REVERSE || + msg.centralConnection === CENTRAL_CONNECTION_DUAL + ) { + offset += CENTRAL_CONNECTION_BASE_OFFSET; + } + + if (msg.centralConnection === CENTRAL_CONNECTION_DUAL && msg.type === BIDIRECTIONAL_SOLID) { + offset += isArrowToRight ? 0 : -CENTRAL_CONNECTION_BIDIRECTIONAL_OFFSET; + } + + return offset; +}; + const buildMessageModel = function (msg, actors, diagObj) { if ( ![ @@ -1644,12 +1694,8 @@ const buildMessageModel = function (msg, actors, diagObj) { let startx = isArrowToRight ? fromRight : fromLeft; let stopx = isArrowToRight ? toLeft : toRight; - if ( - msg.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_REVERSE || - msg.centralConnection === diagObj.db.LINETYPE.CENTRAL_CONNECTION_DUAL - ) { - startx += 4; - } + // Apply central connection positioning adjustments + startx += calculateCentralConnectionOffset(msg, diagObj, isArrowToRight); // As the line width is considered, the left and right values will be off by 2. const isArrowToActivation = Math.abs(toLeft - toRight) > 2;