diff --git a/package.json b/package.json
index 814e9efd..1736148c 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"@types/ramda": "^0.28.20",
"@vue/compat": "^3.2.45",
"antlr4": "~4.11.0",
+ "click-outside-vue3": "^4.0.1",
"color-string": "^1.5.5",
"dom-to-image-more": "^2.13.0",
"dompurify": "^3.1.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7c31ce90..372e6482 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,6 +28,9 @@ importers:
antlr4:
specifier: ~4.11.0
version: 4.11.0
+ click-outside-vue3:
+ specifier: ^4.0.1
+ version: 4.0.1
color-string:
specifier: ^1.5.5
version: 1.5.5
@@ -2564,6 +2567,12 @@ packages:
integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==,
}
+ "@types/node@22.14.0":
+ resolution:
+ {
+ integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==,
+ }
+
"@types/ramda@0.28.20":
resolution:
{
@@ -3688,6 +3697,13 @@ packages:
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
+ click-outside-vue3@4.0.1:
+ resolution:
+ {
+ integrity: sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==,
+ }
+ engines: { node: ">=6" }
+
cliui@8.0.1:
resolution:
{
@@ -7756,6 +7772,12 @@ packages:
integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==,
}
+ undici-types@6.21.0:
+ resolution:
+ {
+ integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==,
+ }
+
unicode-canonical-property-names-ecmascript@2.0.0:
resolution:
{
@@ -9953,6 +9975,10 @@ snapshots:
dependencies:
undici-types: 6.20.0
+ "@types/node@22.14.0":
+ dependencies:
+ undici-types: 6.21.0
+
"@types/ramda@0.28.20":
dependencies:
ts-toolbelt: 6.15.5
@@ -10680,6 +10706,8 @@ snapshots:
slice-ansi: 5.0.0
string-width: 5.1.2
+ click-outside-vue3@4.0.1: {}
+
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -11813,7 +11841,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- "@types/node": 22.10.7
+ "@types/node": 22.14.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -13114,6 +13142,8 @@ snapshots:
undici-types@6.20.0: {}
+ undici-types@6.21.0: {}
+
unicode-canonical-property-names-ecmascript@2.0.0: {}
unicode-match-property-ecmascript@2.0.0:
diff --git a/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ColorPicker.vue b/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ColorPicker.vue
new file mode 100644
index 00000000..97bfb1d6
--- /dev/null
+++ b/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/ColorPicker.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
diff --git a/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLineLayer.vue b/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLineLayer.vue
index c7b59c6b..7d66dff9 100644
--- a/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLineLayer.vue
+++ b/src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLineLayer.vue
@@ -34,6 +34,7 @@
@@ -40,6 +41,11 @@
/>
+
@@ -55,6 +61,7 @@ import { PARTICIPANT_HEIGHT } from "@/positioning/Constants";
import { RenderMode } from "@/store/Store";
import ParticipantLabel from "./ParticipantLabel.vue";
import { _STARTER_ } from "@/parser/OrderedParticipants";
+import ColorPicker from "./ColorPicker.vue";
const INTERSECTION_ERROR_MARGIN = 10; // a threshold for judging whether the participant is intersecting with the viewport
@@ -62,10 +69,21 @@ export default {
name: "Participant",
components: {
ParticipantLabel,
+ ColorPicker,
},
setup(props) {
const store = useStore();
const participant = ref(null);
+ const participantColor = computed({
+ get: () => props.entity.color,
+ set: (value) => {
+ store.dispatch("updateParticipantColor", {
+ color: value,
+ participant: store.getters.participants.Get(props.entity.name),
+ });
+ },
+ });
+
if (store.state.mode === RenderMode.Static) {
return { translate: 0, participant };
}
@@ -106,7 +124,19 @@ export default {
participantOffsetTop
);
});
- return { translate, participant, labelPositions, assigneePositions };
+ // watch(
+ // () => participantColor.value,
+ // (newColor) => {
+ // this.updateFontColor(newColor);
+ // },
+ // );
+ return {
+ translate,
+ participant,
+ labelPositions,
+ assigneePositions,
+ participantColor,
+ };
},
props: {
entity: {
@@ -173,8 +203,8 @@ export default {
onSelect() {
this.$store.commit("onSelect", this.entity.name);
},
- updateFontColor() {
- if (!this.entity.color) {
+ updateFontColor(newColor) {
+ if (!newColor) {
this.color = undefined;
return;
}
diff --git a/src/parser/Participants.ts b/src/parser/Participants.ts
index d361348f..1132fd46 100644
--- a/src/parser/Participants.ts
+++ b/src/parser/Participants.ts
@@ -1,5 +1,21 @@
export type Position = [number, number];
+interface FieldDeclaration {
+ rawText: string;
+ position?: Position;
+}
+
+interface ParticipantDeclaration {
+ participantType?: FieldDeclaration;
+ stereotype?: FieldDeclaration;
+ name: FieldDeclaration;
+ width?: FieldDeclaration;
+ label?: FieldDeclaration;
+ color?: FieldDeclaration;
+ start?: number;
+ stop?: number;
+}
+
interface ParticipantOptions {
isStarter?: boolean;
stereotype?: string;
@@ -13,6 +29,7 @@ interface ParticipantOptions {
assignee?: string;
position?: Position;
assigneePosition?: Position;
+ declaration?: ParticipantDeclaration;
}
export const blankParticipant = {
@@ -29,6 +46,7 @@ export const blankParticipant = {
assignee: undefined,
positions: new Set(),
assigneePositions: new Set(),
+ declaration: undefined,
};
export class Participant {
@@ -40,11 +58,12 @@ export class Participant {
isStarter: boolean | undefined;
label: string | undefined;
private type: string | undefined;
- private color: string | undefined;
+ color: string | undefined;
private comment: string | undefined;
private assignee: string | undefined;
positions: Set = new Set();
assigneePositions: Set = new Set();
+ declaration?: ParticipantDeclaration;
constructor(name: string, options: ParticipantOptions) {
this.name = name;
@@ -63,6 +82,7 @@ export class Participant {
color,
comment,
assignee,
+ declaration,
} = options;
this.stereotype ||= stereotype;
this.width ||= width;
@@ -74,7 +94,9 @@ export class Participant {
this.color ||= color;
this.comment ||= comment;
this.assignee ||= assignee;
+ this.declaration ||= declaration;
}
+
public AddPosition(position: Position) {
this.positions.add(position);
}
@@ -94,6 +116,7 @@ export class Participant {
assignee: this.assignee,
positions: this.positions,
assigneePositions: this.assigneePositions,
+ declaration: this.declaration,
};
}
}
diff --git a/src/parser/ToCollector.js b/src/parser/ToCollector.js
index 571973c8..9f5bea4e 100644
--- a/src/parser/ToCollector.js
+++ b/src/parser/ToCollector.js
@@ -11,35 +11,70 @@ const ToCollector = new sequenceParserListener();
// 1. Later declaration win
// 2. Participant declaration overwrite cannot be overwritten by To or Starter
const onParticipant = function (ctx) {
- // if(!(ctx?.name())) return;
if (isBlind) return;
- const type = ctx?.participantType()?.getFormattedText().replace("@", "");
- const participant =
- ctx?.name()?.getFormattedText() || "Missing `Participant`";
- const stereotype = ctx.stereotype()?.name()?.getFormattedText();
- const width =
- (ctx.width && ctx.width() && Number.parseInt(ctx.width().getText())) ||
- undefined;
+ const typeCtx = ctx?.participantType();
+ const type = typeCtx?.getFormattedText().replace("@", "");
+ const nameCtx = ctx?.name();
+ const participant = nameCtx?.getFormattedText() || "Missing `Participant`";
+ const stereotypeCtx = ctx.stereotype()?.name();
+ const stereotype = stereotypeCtx?.getFormattedText();
+ const widthCtx = ctx.width && ctx.width();
+ const width = widthCtx && Number.parseInt(widthCtx.getText());
const labelCtx = ctx.label && ctx.label();
const label = labelCtx?.name()?.getFormattedText();
const explicit = true;
const color = ctx.COLOR()?.getText();
const comment = ctx.getComment();
- const nameCtx = ctx.name();
- let start, end;
- // When label is present, it means we edit label in diagram and update its code regardless of the occurrence of the participant name
+ const declaration = {
+ name: {
+ rawText: nameCtx?.getText() || "Missing `Participant`",
+ position: [nameCtx?.start.start, nameCtx?.stop.stop + 1],
+ },
+ };
+
+ if (typeCtx) {
+ declaration.participantType = {
+ rawText: typeCtx.getText(),
+ position: [typeCtx.start.start, typeCtx.stop.stop + 1],
+ };
+ }
+
+ if (stereotypeCtx) {
+ declaration.stereotype = {
+ rawText: stereotypeCtx.getText(),
+ position: [stereotypeCtx.start.start, stereotypeCtx.stop.stop + 1],
+ };
+ }
+
+ if (widthCtx) {
+ declaration.width = {
+ rawText: widthCtx.getText(),
+ position: [widthCtx.start.start, widthCtx.stop.stop + 1],
+ };
+ }
+
if (labelCtx) {
const labelNameCtx = labelCtx.name();
if (labelNameCtx) {
- start = labelNameCtx.start.start;
- end = labelNameCtx.stop.stop + 1;
+ declaration.label = {
+ rawText: labelNameCtx.getText(),
+ position: [labelNameCtx.start.start, labelNameCtx.stop.stop + 1],
+ };
}
- } else if (nameCtx) {
- start = nameCtx.start.start;
- end = nameCtx.stop.stop + 1;
}
+ if (color) {
+ declaration.color = {
+ rawText: color,
+ position: [ctx.COLOR().symbol.start, ctx.COLOR().symbol.stop + 1],
+ };
+ }
+
+ declaration.start = ctx.start.start;
+
+ declaration.stop = ctx.stop.stop + 1;
+
participants.Add(participant, {
isStarter: false,
type,
@@ -50,7 +85,10 @@ const onParticipant = function (ctx) {
explicit,
color,
comment,
- position: [start, end],
+ declaration,
+ position: declaration.label
+ ? declaration.label.position
+ : declaration.name.position,
});
};
ToCollector.enterParticipant = onParticipant;
diff --git a/src/parser/index.js b/src/parser/index.js
index 4f2a1053..f3b86027 100644
--- a/src/parser/index.js
+++ b/src/parser/index.js
@@ -35,10 +35,13 @@ function rootContext(code) {
return parser._syntaxErrors ? null : parser.prog();
}
+antlr4.ParserRuleContext.prototype.getRawText = function () {
+ return this.parser.getTokenStream().getText(this.getSourceInterval());
+};
+
antlr4.ParserRuleContext.prototype.getFormattedText = function () {
- const code = this.parser.getTokenStream().getText(this.getSourceInterval());
// remove extra quotes, spaces and new lines
- return formatText(code);
+ return formatText(this.getRawText());
};
// Comment is where users have the most flexibility. The parser should make minimal assumptions about
diff --git a/src/store/Store.spec.ts b/src/store/Store.spec.ts
index 63b4968b..fea53866 100644
--- a/src/store/Store.spec.ts
+++ b/src/store/Store.spec.ts
@@ -1,8 +1,186 @@
-import Store from "./Store";
+import { describe, expect, test, vi } from "vitest";
+import Store, { RenderMode, updateParticipantColorInText } from "./Store";
+import { RootContext } from "@/parser";
+import ToCollector from "@/parser/ToCollector";
describe("Store", () => {
- it("should create an instance", () => {
- expect(Store().state.showTips).toBeDefined();
- expect(Store().state.showTips).toBeFalsy();
+ describe("updateParticipantColor", () => {
+ test("should update color for explicitly declared participant", () => {
+ // Setup initial state
+ const code = "A #000000";
+ const rootContext = RootContext(code);
+ // @ts-ignore
+ const participants = ToCollector.getParticipants(rootContext);
+ const participant = participants.Get("A");
+ const state = {
+ code,
+ participants,
+ mode: RenderMode.Static,
+ scale: 1,
+ selected: [],
+ cursor: null,
+ showTips: false,
+ numbering: false,
+ onElementClick: vi.fn(),
+ };
+
+ // Get the store actions
+ const { actions } = Store();
+ const context = {
+ state,
+ commit: (type: string, payload: any) => {
+ if (type === "code") {
+ state.code = payload;
+ }
+ },
+ };
+
+ // Update color
+ actions.updateParticipantColor(context, {
+ name: "A",
+ color: "#111111",
+ participant,
+ });
+
+ // Only check code was updated - participant updates happen through re-parsing
+ expect(state.code).toBe("A #111111");
+ });
+
+ test("should remove color when color parameter is undefined", () => {
+ // Setup initial state with colored participant
+ const code = "A #000000";
+ const rootContext = RootContext(code);
+ const participants = ToCollector.getParticipants(rootContext);
+ const participant = participants.Get("A");
+ const state = {
+ code,
+ participants,
+ mode: RenderMode.FULL,
+ scale: 1,
+ selected: [],
+ cursor: null,
+ showTips: false,
+ numbering: false,
+ onElementClick: vi.fn(),
+ };
+
+ // Get the store actions
+ const { actions } = Store();
+ const context = {
+ state,
+ commit: (type: string, payload: any) => {
+ if (type === "code") {
+ state.code = payload;
+ }
+ },
+ };
+
+ // Remove color
+ actions.updateParticipantColor(context, {
+ name: "A",
+ color: undefined,
+ participant,
+ });
+
+ // Only check code was updated - participant updates happen through re-parsing
+ expect(state.code).toBe("A");
+ });
+
+ test("should not update color for implicit participant", () => {
+ // Setup initial state with implicit participant
+ const code = "A->B"; // B is implicit
+ const rootContext = RootContext(code);
+ const participants = ToCollector.getParticipants(rootContext);
+ const participant = participants.Get("B");
+ const state = {
+ code,
+ participants,
+ mode: RenderMode.FULL,
+ scale: 1,
+ selected: [],
+ cursor: null,
+ showTips: false,
+ numbering: false,
+ onElementClick: vi.fn(),
+ };
+
+ // Get the store actions
+ const { actions } = Store();
+ const context = {
+ state,
+ commit: (type: string, payload: any) => {
+ if (type === "code") {
+ state.code = payload;
+ }
+ },
+ };
+
+ // Try to update color of implicit participant
+ actions.updateParticipantColor(context, {
+ name: "B",
+ color: "#111111",
+ participant,
+ });
+
+ // Check code wasn't modified
+ expect(state.code).toBe("A->B");
+ });
+
+ test("should handle participant with label", () => {
+ // Setup initial state with labeled participant
+ const code = '"User A" #000000';
+ const rootContext = RootContext(code);
+ const participants = ToCollector.getParticipants(rootContext);
+ const participant = participants.Get("User A");
+ const state = {
+ code,
+ participants,
+ mode: RenderMode.FULL,
+ scale: 1,
+ selected: [],
+ cursor: null,
+ showTips: false,
+ numbering: false,
+ onElementClick: vi.fn(),
+ };
+
+ // Get the store actions
+ const { actions } = Store();
+ const context = {
+ state,
+ commit: (type: string, payload: any) => {
+ if (type === "code") {
+ state.code = payload;
+ }
+ },
+ };
+
+ // Update color
+ actions.updateParticipantColor(context, {
+ name: "User A",
+ color: "#111111",
+ participant,
+ });
+
+ // Only check code was updated - participant updates happen through re-parsing
+ expect(state.code).toBe('"User A" #111111');
+ });
+ });
+
+ describe("updateParticipantColorInText", () => {
+ test.each([
+ ["A", "#000000", "A #000000"],
+ ["B #000000", "#111111", "B #111111"],
+ ["C #000000", undefined, "C"],
+ ['"D"', "#000000", '"D" #000000'],
+ ['"E" #000000', "#111111", '"E" #111111'],
+ ["<> F #000000", "#111111", "<> F #111111"],
+ ["G #000000", "#111111", "G #111111"], // Multiple spaces
+ ["H #000000 ", "#111111", "H #111111"], // Trailing space
+ ["I #000000\n", "#111111", "I #111111"], // Trailing newline
+ ["J #abcdef", "#111111", "J #111111"], // Different color format
+ ])("should update color in '%s' to '%s'", (text, color, expected) => {
+ expect(updateParticipantColorInText(text, color)).toBe(expected);
+ });
});
});
diff --git a/src/store/Store.ts b/src/store/Store.ts
index 1260f9cf..e79f98de 100644
--- a/src/store/Store.ts
+++ b/src/store/Store.ts
@@ -9,6 +9,7 @@ import WidthProviderOnBrowser from "../positioning/WidthProviderFunc";
import { Coordinates } from "@/positioning/Coordinates";
import { CodeRange } from "@/parser/CodeRange";
import { StoreOptions } from "vuex";
+import { Participant } from "@/parser/Participants";
/*
* RenderMode
@@ -149,6 +150,25 @@ const Store = (): StoreOptions => {
scoped: payload,
});
},
+ updateParticipant: function (
+ state: any,
+ payload: { participant: Participant; color?: string },
+ ) {
+ const { participant, color } = payload;
+ // Get the original text range
+ const range = participant.getRange();
+ if (!range) return;
+
+ // Get the text content and update it with the new color
+ const originalText = state.code.substring(range.start, range.end);
+ const updatedText = updateParticipantColorInText(originalText, color);
+
+ // Replace the text in the code
+ state.code =
+ state.code.substring(0, range.start) +
+ updatedText +
+ state.code.substring(range.end);
+ },
eventEmit: function (state: any, payload: any) {
state.onEventEmit?.(payload.event, payload.data);
},
@@ -169,6 +189,44 @@ const Store = (): StoreOptions => {
},
},
actions: {
+ updateParticipantColor(
+ { commit, state },
+ { color, participant }: { color?: string; participant: Participant },
+ ) {
+ console.log("participant:", participant);
+
+ // Only update color if the participant was explicitly declared
+ if (!participant?.explicit || !participant?.declaration) {
+ return;
+ }
+
+ // Get the declaration and its position
+ const declaration = participant.declaration;
+
+ // Find the end of the declaration (either current color position or name position)
+ const declarationStart = declaration.start;
+ const declarationEnd = declaration.stop;
+
+ // Get the full declaration text including any existing color
+ const declarationText = state.code.substring(
+ declarationStart,
+ declarationEnd,
+ );
+ console.log("declarationText:", declarationText);
+
+ // Update the color in the code
+ const beforeCode = state.code.substring(0, declarationStart);
+ const afterCode = state.code.substring(declarationEnd);
+ const updatedDeclarationText = updateParticipantColorInText(
+ declarationText,
+ color,
+ );
+ const newCode = beforeCode + updatedDeclarationText + afterCode;
+
+ // Update code through mutation
+ commit("code", newCode);
+ state.onContentChange?.(newCode);
+ },
// Why debounce is here instead of mutation 'code'?
// Both code and cursor must be mutated together, especially during typing.
updateCode: function ({ commit }: any, payload: any) {
@@ -186,4 +244,17 @@ const Store = (): StoreOptions => {
strict: false,
};
};
+
+// Helper function to update color in participant declaration text
+export function updateParticipantColorInText(
+ text: string,
+ color?: string,
+): string {
+ // Remove any existing color (format: #RRGGBB) and trailing whitespace/newlines
+ const baseText = text.replace(/#[0-9a-fA-F]{6}/g, "").trim();
+
+ // Add new color if provided
+ return color ? `${baseText} ${color}` : baseText;
+}
+
export default Store;
diff --git a/test/unit/parser/to-collector.spec.js b/test/unit/parser/to-collector.spec.js
index 9093e1a1..bfa5f24b 100644
--- a/test/unit/parser/to-collector.spec.js
+++ b/test/unit/parser/to-collector.spec.js
@@ -40,6 +40,20 @@ test("smoke test2", () => {
[32, 37],
[56, 61],
]),
+ declaration: {
+ name: {
+ rawText: '"B 1"',
+ position: [32, 37],
+ },
+ stereotype: {
+ rawText: "A",
+ position: [28, 29],
+ },
+ width: {
+ rawText: "1024",
+ position: [38, 42],
+ },
+ },
});
});
@@ -50,11 +64,29 @@ describe("Plain participants", () => {
// `A` will be parsed as a participant which matches `participant EOF`
let participants = getParticipants(code);
expect(participants.Size()).toBe(1);
- expect(participants.Get("A").width).toBeUndefined();
+ expect(participants.Get("A").width).toBeNull();
expect(participants.Get("A").stereotype).toBeUndefined();
},
);
});
+
+describe("With color", () => {
+ test.each(["A #000000", "A #000000", "<> A #000000"])(
+ "get participant with color and declaration",
+ (code) => {
+ let participants = getParticipants(code);
+ const participant = participants.Get("A");
+ expect(participant.color).toBe("#000000");
+ expect(participant.declaration).toBeDefined();
+ expect(participant.declaration.name.rawText).toBe("A");
+ expect(participant.declaration.color.rawText).toBe("#000000");
+ if (code.includes("<>")) {
+ expect(participant.declaration.stereotype.rawText).toBe("s");
+ }
+ },
+ );
+});
+
describe("with width", () => {
test.each([
["A 1024", 1024],
@@ -133,32 +165,38 @@ describe("with participantType", () => {
["@actor A\nA", "actor"],
["@Actor A", "Actor"],
["@database A", "database"],
- ])("code:%s => participantType:%s", (code, participantType) => {
+ ])("code:%s => type:%s", (code, type) => {
let participants = getParticipants(code);
expect(participants.Size()).toBe(1);
expect(participants.Get("A").name).toBe("A");
- expect(participants.Get("A").type).toBe(participantType);
+ expect(participants.Get("A").type).toBe(type);
});
});
-function getParticipants(code) {
- let rootContext = RootContext(code);
- return ToCollector.getParticipants(rootContext);
-}
-
-describe("Add Starter to participants", () => {
- test("Empty context", () => {
- let rootContext = RootContext("");
- const participants = ToCollector.getParticipants(rootContext);
- expect(participants.Size()).toBe(0);
+describe("with participantType and declaration", () => {
+ test.each([
+ ["@actor A", "actor"],
+ ["@actor A\nA", "actor"],
+ ["@Actor A", "Actor"],
+ ])("code:%s => type:%s", (code, type) => {
+ let participants = getParticipants(code);
+ const participant = participants.Get("A");
+ expect(participant.type).toBe(type);
+ expect(participant.declaration).toBeDefined();
+ expect(participant.declaration.participantType.rawText).toBe("@" + type);
+ expect(participant.declaration.name.rawText).toBe("A");
});
+});
- test("A B->A.m", () => {
- let rootContext = RootContext("A B B->A.m");
- const participants = ToCollector.getParticipants(rootContext);
- expect(participants.Size()).toBe(2);
- expect(participants.Get("B").isStarter).toBeFalsy();
- expect(participants.Names()).toStrictEqual(["A", "B"]);
+describe("without starter", () => {
+ test.each([
+ ["A.method", "A", 1],
+ ["@Starter(A)", "A", 1],
+ ])("code:%s => participant:%s", (code, participant, numberOfParticipants) => {
+ // `A` will be parsed as a participant which matches `participant EOF`
+ let participants = getParticipants(code);
+ expect(participants.Size()).toBe(numberOfParticipants);
+ expect(participants.Get("A").name).toBe(participant);
});
});
@@ -359,3 +397,24 @@ describe("enterRef", () => {
expect(participants.GetPositions("C")).toEqual(new Set([[12, 13]]));
});
});
+
+function getParticipants(code) {
+ let rootContext = RootContext(code);
+ return ToCollector.getParticipants(rootContext);
+}
+
+describe("Add Starter to participants", () => {
+ test("Empty context", () => {
+ let rootContext = RootContext("");
+ const participants = ToCollector.getParticipants(rootContext);
+ expect(participants.Size()).toBe(0);
+ });
+
+ test("A B->A.m", () => {
+ let rootContext = RootContext("A B B->A.m");
+ const participants = ToCollector.getParticipants(rootContext);
+ expect(participants.Size()).toBe(2);
+ expect(participants.Get("B").isStarter).toBeFalsy();
+ expect(participants.Names()).toStrictEqual(["A", "B"]);
+ });
+});