From b6801346ebfd74426eed4c2b58c7cbee7220f5fa Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Wed, 20 Aug 2025 16:13:03 -0500 Subject: [PATCH 1/5] Enhance Flow Start Elements - Introduce processType to ParsedFlow interface for better flow representation. - Implement setFlowStart function to ensure filters, scheduled paths, and capability types are treated as arrays. - Add getFlowStart method in UmlGenerator to generate detailed UML output for flow start nodes, including process type and entry criteria. - Update tests to validate handling of various flow start configurations, ensuring accurate UML generation for single and multiple elements. - Add new test cases for flows with no start elements, ensuring graceful handling in UML generation. --- src/main/flow_parser.ts | 30 ++ src/main/flow_types.ts | 6 +- src/main/uml_generator.ts | 110 +++- src/test/flow_parser_test.ts | 119 +++++ .../multiple_start_elements.flow-meta.xml | 59 ++ .../goldens/no_start_elements.flow-meta.xml | 28 + .../goldens/single_capability.flow-meta.xml | 31 ++ src/test/goldens/single_filter.flow-meta.xml | 36 ++ .../single_scheduled_path.flow-meta.xml | 33 ++ src/test/graphviz_generator_test.ts | 3 +- src/test/mermaid_generator_test.ts | 11 +- src/test/uml_generator_test.ts | 502 +++++++++++++++++- 12 files changed, 959 insertions(+), 9 deletions(-) create mode 100644 src/test/goldens/multiple_start_elements.flow-meta.xml create mode 100644 src/test/goldens/no_start_elements.flow-meta.xml create mode 100644 src/test/goldens/single_capability.flow-meta.xml create mode 100644 src/test/goldens/single_filter.flow-meta.xml create mode 100644 src/test/goldens/single_scheduled_path.flow-meta.xml diff --git a/src/main/flow_parser.ts b/src/main/flow_parser.ts index 4f972d5..8b9fe7a 100644 --- a/src/main/flow_parser.ts +++ b/src/main/flow_parser.ts @@ -67,6 +67,7 @@ export interface Transition { */ export interface ParsedFlow { label?: string; + processType?: flowTypes.FlowProcessType; start?: flowTypes.FlowStart; apexPluginCalls?: flowTypes.FlowApexPluginCall[]; assignments?: flowTypes.FlowAssignment[]; @@ -131,8 +132,10 @@ export class FlowParser { private populateFlowNodes(flow: flowTypes.Flow) { this.beingParsed.label = flow.label; + this.beingParsed.processType = flow.processType; this.beingParsed.start = flow.start; this.validateFlowStart(); + setFlowStart(this.beingParsed.start); this.beingParsed.apexPluginCalls = ensureArray(flow.apexPluginCalls); this.beingParsed.assignments = ensureArray(flow.assignments); @@ -570,6 +573,33 @@ function setCustomErrorMessages( } } +/** + * Set Flow Start + * + * Flow Start filters and scheduled paths are nested properties which also + * need to be converted to arrays. + */ +function setFlowStart(start: flowTypes.FlowStart | undefined) { + if (!start) { + return; + } + if (start.filters) { + start.filters = ensureArray( + start.filters, + ) as flowTypes.FlowRecordFilter[]; + } + if (start.scheduledPaths) { + start.scheduledPaths = ensureArray( + start.scheduledPaths, + ) as flowTypes.FlowScheduledPath[]; + } + if (start.capabilityTypes) { + start.capabilityTypes = ensureArray( + start.capabilityTypes, + ) as flowTypes.FlowCapability[]; + } +} + /** * The following functions are used to determine if a node is a specific type * of node. diff --git a/src/main/flow_types.ts b/src/main/flow_types.ts index c202ff9..cfe5d62 100644 --- a/src/main/flow_types.ts +++ b/src/main/flow_types.ts @@ -714,7 +714,7 @@ export interface FlowScheduledPath extends FlowElement { connector: FlowConnector; label: string; maxBatchSize?: number; // Default is 200 - offsetNumber: number; // Can be positive or negative + offsetNumber: string; // XML parser returns string values for numbers offsetUnit: FlowScheduledPathOffsetUnit; pathType?: FlowScheduledPathType; // Default is null recordField?: string; // Required if timeSource is RecordField @@ -824,13 +824,13 @@ export interface FlowActionCallOutputParameter { export interface FlowElementReferenceOrValue { apexValue?: string; - booleanValue?: boolean; + booleanValue?: string; // XML parser returns string values for booleans dateTimeValue?: string; // Assuming dateTime is represented as a string dateValue?: string; // Assuming date is represented as a string elementReference?: string; formulaDataType?: FormulaDataType; formulaExpression?: string; - numberValue?: number; // using number to represent double + numberValue?: string; // XML parser returns string values for numbers setupReference?: string; setupReferenceType?: string; sobjectValue?: string; diff --git a/src/main/uml_generator.ts b/src/main/uml_generator.ts index 4339e17..155a200 100644 --- a/src/main/uml_generator.ts +++ b/src/main/uml_generator.ts @@ -92,6 +92,7 @@ export abstract class UmlGenerator { generateUml(): string { const result = [ this.getHeader(this.parsedFlow.label!), + this.getFlowStart(), this.processFlowElements( this.parsedFlow.apexPluginCalls, (node) => this.getFlowApexPluginCall(node), @@ -175,6 +176,109 @@ export abstract class UmlGenerator { abstract getTransition(transition: Transition): string; abstract getFooter(): string; + private getFlowStart(): string { + if (!this.parsedFlow.start) { + return ""; + } + + const start = this.parsedFlow.start; + const entryCriteria: string[] = []; + + // Add process type information from the flow + if (this.parsedFlow.processType) { + entryCriteria.push(`Process Type: ${this.parsedFlow.processType}`); + } + + // Add trigger type information + if (start.triggerType) { + entryCriteria.push(`Trigger Type: ${start.triggerType}`); + } + + // Add object information for record-triggered flows + if (start.object) { + entryCriteria.push(`Object: ${start.object}`); + } + + // Add record trigger type information + if (start.recordTriggerType) { + entryCriteria.push(`Record Trigger: ${start.recordTriggerType}`); + } + + // Add entry type information + if (start.entryType) { + entryCriteria.push(`Entry Type: ${start.entryType}`); + } + + // Add filter information + if (start.filterLogic && start.filters && start.filters.length > 0) { + entryCriteria.push(`Filter Logic: ${start.filterLogic}`); + start.filters.forEach((filter, index) => { + entryCriteria.push(`${index + 1}. ${filter.field} ${filter.operator} ${toString(filter.value)}`); + }); + } + + // Add filter formula if present + if (start.filterFormula) { + entryCriteria.push(`Filter Formula: ${start.filterFormula}`); + } + + // Add schedule information for scheduled flows + if (start.schedule) { + entryCriteria.push(`Schedule: ${start.schedule.frequency} starting ${start.schedule.startDate} at ${start.schedule.startTime}`); + } + + // Add scheduled paths information + if (start.scheduledPaths && start.scheduledPaths.length > 0) { + start.scheduledPaths.forEach((path, index) => { + entryCriteria.push(`Scheduled Path ${index + 1}: ${path.label} (${path.offsetNumber} ${path.offsetUnit})`); + }); + } + + // Add capability information + if (start.capabilityTypes && start.capabilityTypes.length > 0) { + start.capabilityTypes.forEach((capability, index) => { + entryCriteria.push(`Capability ${index + 1}: ${capability.capabilityName}`); + }); + } + + // Add form information for form-triggered flows + if (start.form) { + entryCriteria.push(`Form: ${start.form}`); + } + + // Add segment information + if (start.segment) { + entryCriteria.push(`Segment: ${start.segment}`); + } + + // Add flow run as user information + if (start.flowRunAsUser) { + entryCriteria.push(`Run As: ${start.flowRunAsUser}`); + } + + // If no specific criteria found, add a default message + if (entryCriteria.length === 0) { + entryCriteria.push("No specific entry criteria defined"); + } + + return this.toUmlString({ + id: "FLOW_START", + label: "Flow Start", + type: "Flow Start", + color: SkinColor.NONE, + icon: Icon.NONE, + diffStatus: start.diffStatus, + innerNodes: [ + { + id: "FlowStart__EntryCriteria", + type: "Flow Details", + label: "", + content: entryCriteria, + }, + ], + }); + } + private getFlowApexPluginCall(node: flowTypes.FlowApexPluginCall): string { return this.toUmlString({ id: node.name, @@ -631,10 +735,10 @@ function toString(element: flowTypes.FlowElementReferenceOrValue | undefined) { return new Date(element.dateValue).toLocaleDateString(); } if (element.numberValue) { - return element.numberValue.toString(); + return element.numberValue; } - if (element.booleanValue) { - return element.booleanValue ? "true" : "false"; + if (element.booleanValue !== undefined) { + return element.booleanValue; } return ""; } diff --git a/src/test/flow_parser_test.ts b/src/test/flow_parser_test.ts index 65a486d..ec33779 100644 --- a/src/test/flow_parser_test.ts +++ b/src/test/flow_parser_test.ts @@ -35,6 +35,11 @@ const TEST_FILES = { ), circularTransition: join(GOLDENS_PATH, "circular_transition.flow-meta.xml"), rollback: join(GOLDENS_PATH, "rollback.flow-meta.xml"), + singleFilter: join(GOLDENS_PATH, "single_filter.flow-meta.xml"), + singleScheduledPath: join(GOLDENS_PATH, "single_scheduled_path.flow-meta.xml"), + singleCapability: join(GOLDENS_PATH, "single_capability.flow-meta.xml"), + multipleStartElements: join(GOLDENS_PATH, "multiple_start_elements.flow-meta.xml"), + noStartElements: join(GOLDENS_PATH, "no_start_elements.flow-meta.xml"), }; const NODE_NAMES = { @@ -389,4 +394,118 @@ Deno.test("FlowParser", async (t) => { ); }, ); + + await t.step( + "should ensure flow start filters are always treated as arrays", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.singleFilter), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.start); + assert(parsedFlow.start.filters); + assert(Array.isArray(parsedFlow.start.filters)); + assertEquals(parsedFlow.start.filters.length, 1); + assertEquals(parsedFlow.start.filters[0].field, "Name"); + assertEquals(parsedFlow.start.filters[0].operator, "IsNull"); + assertEquals(parsedFlow.start.filters[0].value.booleanValue, "true"); + }, + ); + + await t.step( + "should ensure flow start scheduled paths are always treated as arrays", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.singleScheduledPath), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.start); + assert(parsedFlow.start.scheduledPaths); + assert(Array.isArray(parsedFlow.start.scheduledPaths)); + assertEquals(parsedFlow.start.scheduledPaths.length, 1); + assertEquals(parsedFlow.start.scheduledPaths[0].label, "Daily"); + assertEquals(parsedFlow.start.scheduledPaths[0].offsetNumber, "1"); + assertEquals(parsedFlow.start.scheduledPaths[0].offsetUnit, "Days"); + }, + ); + + await t.step( + "should ensure flow start capability types are always treated as arrays", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.singleCapability), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.start); + assert(parsedFlow.start.capabilityTypes); + assert(Array.isArray(parsedFlow.start.capabilityTypes)); + assertEquals(parsedFlow.start.capabilityTypes.length, 1); + assertEquals(parsedFlow.start.capabilityTypes[0].capabilityName, "Chatter"); + }, + ); + + await t.step( + "should ensure flow start with multiple filters, scheduled paths, and capabilities are handled correctly", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.multipleStartElements), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.start); + + // Check filters + assert(parsedFlow.start.filters); + assert(Array.isArray(parsedFlow.start.filters)); + assertEquals(parsedFlow.start.filters.length, 2); + assertEquals(parsedFlow.start.filters[0].field, "Name"); + assertEquals(parsedFlow.start.filters[0].operator, "IsNull"); + assertEquals(parsedFlow.start.filters[0].value.booleanValue, "true"); + assertEquals(parsedFlow.start.filters[1].field, "Type"); + assertEquals(parsedFlow.start.filters[1].operator, "EqualTo"); + assertEquals(parsedFlow.start.filters[1].value.stringValue, "Prospect"); + + // Check scheduled paths + assert(parsedFlow.start.scheduledPaths); + assert(Array.isArray(parsedFlow.start.scheduledPaths)); + assertEquals(parsedFlow.start.scheduledPaths.length, 2); + assertEquals(parsedFlow.start.scheduledPaths[0].label, "Daily"); + assertEquals(parsedFlow.start.scheduledPaths[0].offsetNumber, "1"); + assertEquals(parsedFlow.start.scheduledPaths[0].offsetUnit, "Days"); + assertEquals(parsedFlow.start.scheduledPaths[1].label, "Weekly"); + assertEquals(parsedFlow.start.scheduledPaths[1].offsetNumber, "7"); + assertEquals(parsedFlow.start.scheduledPaths[1].offsetUnit, "Days"); + + // Check capability types + assert(parsedFlow.start.capabilityTypes); + assert(Array.isArray(parsedFlow.start.capabilityTypes)); + assertEquals(parsedFlow.start.capabilityTypes.length, 2); + assertEquals(parsedFlow.start.capabilityTypes[0].capabilityName, "Chatter"); + assertEquals(parsedFlow.start.capabilityTypes[1].capabilityName, "Lightning"); + }, + ); + + await t.step( + "should handle flow start with no filters, scheduled paths, or capabilities gracefully", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.noStartElements), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.start); + // These should be undefined when not present in XML + assertEquals(parsedFlow.start.filters, undefined); + assertEquals(parsedFlow.start.scheduledPaths, undefined); + assertEquals(parsedFlow.start.capabilityTypes, undefined); + }, + ); }); diff --git a/src/test/goldens/multiple_start_elements.flow-meta.xml b/src/test/goldens/multiple_start_elements.flow-meta.xml new file mode 100644 index 0000000..410dc3c --- /dev/null +++ b/src/test/goldens/multiple_start_elements.flow-meta.xml @@ -0,0 +1,59 @@ + + + + AutoLaunchedFlow + + 50 + 0 + + Assignment1 + + and + + Name + IsNull + + true + + + + Type + EqualTo + + Prospect + + + + + 1 + Days + + + + 7 + Days + + + Chatter + + + Lightning + + + + Assignment1 + + 200 + 0 + + $Record + Description + + Updated + + + + END + + + diff --git a/src/test/goldens/no_start_elements.flow-meta.xml b/src/test/goldens/no_start_elements.flow-meta.xml new file mode 100644 index 0000000..333ddfa --- /dev/null +++ b/src/test/goldens/no_start_elements.flow-meta.xml @@ -0,0 +1,28 @@ + + + + AutoLaunchedFlow + + 50 + 0 + + Assignment1 + + + + Assignment1 + + 200 + 0 + + $Record + Description + + Updated + + + + END + + + diff --git a/src/test/goldens/single_capability.flow-meta.xml b/src/test/goldens/single_capability.flow-meta.xml new file mode 100644 index 0000000..e0875db --- /dev/null +++ b/src/test/goldens/single_capability.flow-meta.xml @@ -0,0 +1,31 @@ + + + + AutoLaunchedFlow + + 50 + 0 + + Assignment1 + + + Chatter + + + + Assignment1 + + 200 + 0 + + $Record + Description + + Updated + + + + END + + + diff --git a/src/test/goldens/single_filter.flow-meta.xml b/src/test/goldens/single_filter.flow-meta.xml new file mode 100644 index 0000000..b434692 --- /dev/null +++ b/src/test/goldens/single_filter.flow-meta.xml @@ -0,0 +1,36 @@ + + + + AutoLaunchedFlow + + 50 + 0 + + Assignment1 + + and + + Name + IsNull + + true + + + + + Assignment1 + + 200 + 0 + + $Record + Description + + Updated + + + + END + + + diff --git a/src/test/goldens/single_scheduled_path.flow-meta.xml b/src/test/goldens/single_scheduled_path.flow-meta.xml new file mode 100644 index 0000000..b96ffaa --- /dev/null +++ b/src/test/goldens/single_scheduled_path.flow-meta.xml @@ -0,0 +1,33 @@ + + + + AutoLaunchedFlow + + 50 + 0 + + Assignment1 + + + + 1 + Days + + + + Assignment1 + + 200 + 0 + + $Record + Description + + Updated + + + + END + + + diff --git a/src/test/graphviz_generator_test.ts b/src/test/graphviz_generator_test.ts index 18edb3d..764aa14 100644 --- a/src/test/graphviz_generator_test.ts +++ b/src/test/graphviz_generator_test.ts @@ -163,8 +163,9 @@ function generateDecision(name: string): flowTypes.FlowDecision { leftValueReference: "foo", operator: flowTypes.FlowComparisonOperator.EQUAL_TO, rightValue: { - booleanValue: true, + booleanValue: "true", }, + processMetadataValues: [], }, ], }, diff --git a/src/test/mermaid_generator_test.ts b/src/test/mermaid_generator_test.ts index 8953ec9..2727949 100644 --- a/src/test/mermaid_generator_test.ts +++ b/src/test/mermaid_generator_test.ts @@ -145,10 +145,15 @@ function generateDecision(name: string): flowTypes.FlowDecision { return { name: name, label: name, + elementSubtype: "Decision", + locationX: 0, + locationY: 0, + description: name, rules: [ { name: `${name}_rule1`, label: "Rule 1", + description: "Rule 1 description", conditions: [ { leftValueReference: "leftValue", @@ -156,12 +161,14 @@ function generateDecision(name: string): flowTypes.FlowDecision { rightValue: { stringValue: "rightValue", }, + processMetadataValues: [], }, ], }, { name: `${name}_rule2`, label: "Rule 2", + description: "Rule 2 description", conditions: [ { leftValueReference: "anotherLeftValue", @@ -169,13 +176,15 @@ function generateDecision(name: string): flowTypes.FlowDecision { rightValue: { stringValue: "anotherRightValue", }, + processMetadataValues: [], }, { leftValueReference: "thirdLeftValue", operator: "GreaterThan", rightValue: { - numberValue: 10, + numberValue: "10", }, + processMetadataValues: [], }, ], conditionLogic: "1 AND 2", diff --git a/src/test/uml_generator_test.ts b/src/test/uml_generator_test.ts index 5263077..8128b3f 100644 --- a/src/test/uml_generator_test.ts +++ b/src/test/uml_generator_test.ts @@ -45,6 +45,10 @@ const NODE_NAMES = { }; const UML_REPRESENTATIONS = { + flowStart: () => + `state Flow Start FLOW_START + Flow Details + No specific entry criteria defined`, apexPluginCall: (name: string) => `state Apex Plugin Call ${name}`, assignment: (name: string) => `state Assignment ${name} @@ -270,6 +274,7 @@ Deno.test("UmlGenerator", async (t) => { const expectedUml = [ NODE_NAMES.label, + UML_REPRESENTATIONS.flowStart(), UML_REPRESENTATIONS.apexPluginCall(NODE_NAMES.apexPluginCall), UML_REPRESENTATIONS.assignment(NODE_NAMES.assignment), UML_REPRESENTATIONS.collectionProcessor(NODE_NAMES.collectionProcessor), @@ -593,7 +598,7 @@ Deno.test("UmlGenerator", async (t) => { { assignToReference: "var2", operator: flowTypes.FlowAssignmentOperator.ADD, - value: { numberValue: 42 }, + value: { numberValue: "42" }, processMetadataValues: [], }, { @@ -631,4 +636,499 @@ Deno.test("UmlGenerator", async (t) => { }); }, ); + + await t.step( + "should generate AutoLaunchedFlow start node with process type", + () => { + // Setup test data for AutoLaunchedFlow + mockParsedFlow.processType = flowTypes.FlowProcessType.AUTOLAUNCHED_FLOW; + mockParsedFlow.start = { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "AutoLaunched flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + }; + + const uml = systemUnderTest.generateUml(); + + const expectedContent = [ + "Flow Start FLOW_START", + "Flow Details", + "Process Type: AutoLaunchedFlow", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should generate record-triggered flow start node with filters", + () => { + // Setup test data for record-triggered flow + mockParsedFlow.processType = flowTypes.FlowProcessType.FLOW; + mockParsedFlow.start = { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Record-triggered flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + triggerType: flowTypes.FlowTriggerType.RECORD_AFTER_SAVE, + object: "Account", + recordTriggerType: flowTypes.RecordTriggerType.CREATE_AND_UPDATE, + entryType: flowTypes.FlowEntryType.ALWAYS, + filterLogic: "1 AND 2", + filters: [ + { + field: "Type", + operator: flowTypes.FlowRecordFilterOperator.EQUAL_TO, + value: { stringValue: "Customer" }, + }, + { + field: "Status", + operator: flowTypes.FlowRecordFilterOperator.NOT_EQUAL_TO, + value: { stringValue: "Inactive" }, + }, + ], + filterFormula: "{!$Record.Type} = 'Customer' && {!$Record.Status} != 'Inactive'", + }; + + const uml = systemUnderTest.generateUml(); + + const expectedContent = [ + "Process Type: Flow", + "Trigger Type: RecordAfterSave", + "Object: Account", + "Record Trigger: CreateAndUpdate", + "Entry Type: Always", + "Filter Logic: 1 AND 2", + "1. Type EqualTo Customer", + "2. Status NotEqualTo Inactive", + "Filter Formula: {!$Record.Type} = 'Customer' && {!$Record.Status} != 'Inactive'", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should generate scheduled flow start node with schedule information", + () => { + // Setup test data for scheduled flow + mockParsedFlow.processType = flowTypes.FlowProcessType.AUTOLAUNCHED_FLOW; + mockParsedFlow.start = { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Scheduled flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + triggerType: flowTypes.FlowTriggerType.SCHEDULED, + schedule: { + frequency: flowTypes.FlowStartFrequency.DAILY, + startDate: "2024-01-01", + startTime: "09:00:00", + }, + scheduledPaths: [ + { + name: "DailyPath", + label: "Daily Processing", + connector: { targetReference: "END", isGoTo: false }, + offsetNumber: "1", + offsetUnit: flowTypes.FlowScheduledPathOffsetUnit.DAYS, + timeSource: flowTypes.FlowScheduledPathTimeSource.RECORD_TRIGGER_EVENT, + description: "Daily scheduled path", + }, + { + name: "WeeklyPath", + label: "Weekly Report", + connector: { targetReference: "END", isGoTo: false }, + offsetNumber: "7", + offsetUnit: flowTypes.FlowScheduledPathOffsetUnit.DAYS, + timeSource: flowTypes.FlowScheduledPathTimeSource.RECORD_FIELD, + description: "Weekly scheduled path", + }, + ], + }; + + const uml = systemUnderTest.generateUml(); + + const expectedContent = [ + "Process Type: AutoLaunchedFlow", + "Trigger Type: Scheduled", + "Schedule: Daily starting 2024-01-01 at 09:00:00", + "Scheduled Path 1: Daily Processing (1 Days)", + "Scheduled Path 2: Weekly Report (7 Days)", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should generate platform event triggered flow start node", + () => { + // Setup test data for platform event flow + mockParsedFlow.processType = flowTypes.FlowProcessType.AUTOLAUNCHED_FLOW; + mockParsedFlow.start = { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Platform event triggered flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + triggerType: flowTypes.FlowTriggerType.PLATFORM_EVENT, + object: "Order_Event__e", + segment: "High_Value_Orders", + flowRunAsUser: "admin@example.com", + }; + + const uml = systemUnderTest.generateUml(); + + const expectedContent = [ + "Process Type: AutoLaunchedFlow", + "Trigger Type: PlatformEvent", + "Object: Order_Event__e", + "Segment: High_Value_Orders", + "Run As: admin@example.com", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should generate flow start node with capabilities and form", + () => { + // Setup test data for capability-based flow + mockParsedFlow.processType = flowTypes.FlowProcessType.FLOW; + mockParsedFlow.start = { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Capability-based flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + triggerType: flowTypes.FlowTriggerType.CAPABILITY, + capabilityTypes: [ + { + name: "DataProcessing", + capabilityName: "Data Processing Capability", + inputs: [ + { + name: "InputData", + capabilityInputName: "Data Input", + dataType: "sObject", + isCollection: true, + description: "Input data collection", + }, + ], + description: "Data processing capability", + }, + { + name: "ValidationCapability", + capabilityName: "Validation Rules", + inputs: [ + { + name: "RecordToValidate", + capabilityInputName: "Record Input", + dataType: "sObject", + isCollection: false, + description: "Record to validate", + }, + ], + description: "Validation capability", + }, + ], + form: "Custom_Lead_Form", + segment: "Enterprise_Customers", + }; + + const uml = systemUnderTest.generateUml(); + + const expectedContent = [ + "Process Type: Flow", + "Trigger Type: Capability", + "Capability 1: Data Processing Capability", + "Capability 2: Validation Rules", + "Form: Custom_Lead_Form", + "Segment: Enterprise_Customers", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should handle minimal start node configuration", + () => { + // Create a minimal mock flow with just the start node + const minimalMockFlow: ParsedFlow = { + label: "Minimal Test", + processType: undefined, + start: { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Minimal flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + }, + transitions: [], + }; + + // Create a fresh generator with the minimal mock flow + class ConcreteUmlGenerator extends UmlGenerator { + getHeader(label: string): string { + return label; + } + toUmlString(node: DiagramNode): string { + let result = `state ${node.type} ${node.id}`; + if (node.innerNodes) { + const innerContent = node.innerNodes + .map((innerNode) => { + const header = [innerNode.type, innerNode.label] + .filter(Boolean) + .join(": "); + + const content = innerNode.content + .map((line) => ` ${line}`) + .join(EOL); + + return header ? ` ${header}${EOL}${content}` : content; + }) + .join(EOL); + result += EOL + innerContent; + } + return result; + } + getTransition(transition: Transition): string { + return UML_REPRESENTATIONS.transition(transition.from, transition.to); + } + getFooter(): string { + return ""; + } + } + + const minimalGenerator = new ConcreteUmlGenerator(minimalMockFlow); + const uml = minimalGenerator.generateUml(); + + const expectedContent = [ + "Flow Start FLOW_START", + "Flow Details", + "No specific entry criteria defined", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + + // Should not contain any specific trigger information + const unexpectedContent = [ + "Process Type:", + "Trigger Type:", + "Filter Logic:", + ]; + + unexpectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + false, + `Expected UML: ${uml} to NOT contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should handle start node with record change criteria", + () => { + // Create a record change mock flow with just the start node + const recordChangeMockFlow: ParsedFlow = { + label: "Record Change Test", + processType: flowTypes.FlowProcessType.FLOW, + start: { + name: "FLOW_START", + label: "Flow Start", + locationX: 0, + locationY: 0, + elementSubtype: "Start", + description: "Record change flow start", + connector: { targetReference: "nextNode", isGoTo: false }, + triggerType: flowTypes.FlowTriggerType.RECORD_BEFORE_SAVE, + object: "Opportunity", + recordTriggerType: flowTypes.RecordTriggerType.UPDATE, + entryType: flowTypes.FlowEntryType.ALWAYS, + doesRequireRecordChangedToMeetCriteria: true, + filterLogic: "1 OR 2", + filters: [ + { + field: "StageName", + operator: flowTypes.FlowRecordFilterOperator.EQUAL_TO, + value: { stringValue: "Closed Won" }, + }, + { + field: "Amount", + operator: flowTypes.FlowRecordFilterOperator.GREATER_THAN, + value: { numberValue: "100000" }, + }, + ], + }, + transitions: [], + }; + + // Create a fresh generator with the record change mock flow + class ConcreteUmlGenerator extends UmlGenerator { + getHeader(label: string): string { + return label; + } + toUmlString(node: DiagramNode): string { + let result = `state ${node.type} ${node.id}`; + if (node.innerNodes) { + const innerContent = node.innerNodes + .map((innerNode) => { + const header = [innerNode.type, innerNode.label] + .filter(Boolean) + .join(": "); + + const content = innerNode.content + .map((line) => ` ${line}`) + .join(EOL); + + return header ? ` ${header}${EOL}${content}` : content; + }) + .join(EOL); + result += EOL + innerContent; + } + return result; + } + getTransition(transition: Transition): string { + return UML_REPRESENTATIONS.transition(transition.from, transition.to); + } + getFooter(): string { + return ""; + } + } + + const recordChangeGenerator = new ConcreteUmlGenerator(recordChangeMockFlow); + const uml = recordChangeGenerator.generateUml(); + + const expectedContent = [ + "Process Type: Flow", + "Trigger Type: RecordBeforeSave", + "Object: Opportunity", + "Record Trigger: Update", + "Entry Type: Always", + "Filter Logic: 1 OR 2", + "1. StageName EqualTo Closed Won", + "2. Amount GreaterThan 100000", + ]; + + expectedContent.forEach((content) => { + assertEquals( + uml.includes(content), + true, + `Expected UML: ${uml} to contain: ${content}`, + ); + }); + }, + ); + + await t.step( + "should handle empty start node gracefully", + () => { + // Create an empty mock flow with no start node + const emptyMockFlow: ParsedFlow = { + label: "Empty Test", + processType: undefined, + start: undefined, + transitions: [], + }; + + // Create a fresh generator with the empty mock flow + class ConcreteUmlGenerator extends UmlGenerator { + getHeader(label: string): string { + return label; + } + toUmlString(node: DiagramNode): string { + let result = `state ${node.type} ${node.id}`; + if (node.innerNodes) { + const innerContent = node.innerNodes + .map((innerNode) => { + const header = [innerNode.type, innerNode.label] + .filter(Boolean) + .join(": "); + + const content = innerNode.content + .map((line) => ` ${line}`) + .join(EOL); + + return header ? ` ${header}${EOL}${content}` : content; + }) + .join(EOL); + result += EOL + innerContent; + } + return result; + } + getTransition(transition: Transition): string { + return UML_REPRESENTATIONS.transition(transition.from, transition.to); + } + getFooter(): string { + return ""; + } + } + + const emptyGenerator = new ConcreteUmlGenerator(emptyMockFlow); + const uml = emptyGenerator.generateUml(); + + // Should not contain flow start node when undefined + assertEquals( + uml.includes("Flow Start FLOW_START"), + false, + "Should not contain flow start node when undefined", + ); + }, + ); }); From 45a2c4e5adcbb138d72aeb58c1e3805f3914f038 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Wed, 20 Aug 2025 16:23:33 -0500 Subject: [PATCH 2/5] Enhance FlowParser to Support Async Paths - Introduce getTransitionsFromFlowStart method to handle transitions from flow start nodes, including main and scheduled paths. - Update getTransitionsForNode method to incorporate logic for flow start nodes. - Add new test cases to validate the handling of single and multiple async paths, ensuring correct transition generation. - Include tests for non-async scheduled paths to confirm proper labeling behavior. --- src/main/flow_parser.ts | 35 +++++- src/test/flow_parser_test.ts | 101 ++++++++++++++++++ .../goldens/async_path_test.flow-meta.xml | 77 +++++++++++++ .../multiple_async_paths.flow-meta.xml | 96 +++++++++++++++++ .../non_async_scheduled_path.flow-meta.xml | 77 +++++++++++++ 5 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 src/test/goldens/async_path_test.flow-meta.xml create mode 100644 src/test/goldens/multiple_async_paths.flow-meta.xml create mode 100644 src/test/goldens/non_async_scheduled_path.flow-meta.xml diff --git a/src/main/flow_parser.ts b/src/main/flow_parser.ts index 8b9fe7a..ae9faec 100644 --- a/src/main/flow_parser.ts +++ b/src/main/flow_parser.ts @@ -260,7 +260,9 @@ export class FlowParser { */ private getTransitionsForNode(node: flowTypes.FlowNode): Transition[] { const transitions: Transition[] = []; - if ( + if (isFlowStart(node)) { + transitions.push(...this.getTransitionsFromFlowStart(node)); + } else if ( isRecordCreate(node) || isRecordDelete(node) || isRecordLookup(node) || @@ -434,6 +436,31 @@ export class FlowParser { } return result; } + + private getTransitionsFromFlowStart(node: flowTypes.FlowStart): Transition[] { + const result: Transition[] = []; + + // Add main flow transition + if (node.connector) { + result.push( + this.createTransition(node, node.connector, false, undefined), + ); + } + + // Add scheduled path transitions + if (node.scheduledPaths && node.scheduledPaths.length > 0) { + for (const scheduledPath of node.scheduledPaths) { + if (scheduledPath.connector) { + const label = scheduledPath.pathType; + result.push( + this.createTransition(node, scheduledPath.connector, false, label), + ); + } + } + } + + return result; + } } /** @@ -696,3 +723,9 @@ function isCustomError( ): node is flowTypes.FlowCustomError { return (node as flowTypes.FlowCustomError).customErrorMessages !== undefined; } + +function isFlowStart( + node: flowTypes.FlowNode, +): node is flowTypes.FlowStart { + return (node as flowTypes.FlowStart).connector !== undefined; +} diff --git a/src/test/flow_parser_test.ts b/src/test/flow_parser_test.ts index ec33779..e048167 100644 --- a/src/test/flow_parser_test.ts +++ b/src/test/flow_parser_test.ts @@ -40,6 +40,9 @@ const TEST_FILES = { singleCapability: join(GOLDENS_PATH, "single_capability.flow-meta.xml"), multipleStartElements: join(GOLDENS_PATH, "multiple_start_elements.flow-meta.xml"), noStartElements: join(GOLDENS_PATH, "no_start_elements.flow-meta.xml"), + asyncPathTest: join(GOLDENS_PATH, "async_path_test.flow-meta.xml"), + multipleAsyncPaths: join(GOLDENS_PATH, "multiple_async_paths.flow-meta.xml"), + nonAsyncScheduledPath: join(GOLDENS_PATH, "non_async_scheduled_path.flow-meta.xml"), }; const NODE_NAMES = { @@ -508,4 +511,102 @@ Deno.test("FlowParser", async (t) => { assertEquals(parsedFlow.start.capabilityTypes, undefined); }, ); + + await t.step( + "should handle single async path correctly", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.asyncPathTest), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.transitions); + assertEquals(parsedFlow.transitions.length, 2); + + // Main flow transition (no label) + assertEquals(parsedFlow.transitions[0], { + from: START_NODE_NAME, + to: "main_update", + fault: false, + label: undefined, + }); + + // Async path transition (with path type as label) + assertEquals(parsedFlow.transitions[1], { + from: START_NODE_NAME, + to: "async_update", + fault: false, + label: "AsyncAfterCommit", + }); + }, + ); + + await t.step( + "should handle multiple async paths correctly", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.multipleAsyncPaths), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.transitions); + assertEquals(parsedFlow.transitions.length, 3); + + // Main flow transition (no label) + assertEquals(parsedFlow.transitions[0], { + from: START_NODE_NAME, + to: "main_update", + fault: false, + label: undefined, + }); + + // First async path transition + assertEquals(parsedFlow.transitions[1], { + from: START_NODE_NAME, + to: "async_update_1", + fault: false, + label: "AsyncAfterCommit", + }); + + // Second async path transition + assertEquals(parsedFlow.transitions[2], { + from: START_NODE_NAME, + to: "async_update_2", + fault: false, + label: "AsyncAfterCommit", + }); + }, + ); + + await t.step( + "should not label non-async scheduled paths as asynchronous", + async () => { + systemUnderTest = new FlowParser( + Deno.readTextFileSync(TEST_FILES.nonAsyncScheduledPath), + ); + + parsedFlow = await systemUnderTest.generateFlowDefinition(); + + assert(parsedFlow.transitions); + assertEquals(parsedFlow.transitions.length, 2); + + // Main flow transition (no label) + assertEquals(parsedFlow.transitions[0], { + from: START_NODE_NAME, + to: "main_update", + fault: false, + label: undefined, + }); + + // Non-async scheduled path transition (with path type as label) + assertEquals(parsedFlow.transitions[1], { + from: START_NODE_NAME, + to: "scheduled_update", + fault: false, + label: "RecordField", + }); + }, + ); }); diff --git a/src/test/goldens/async_path_test.flow-meta.xml b/src/test/goldens/async_path_test.flow-meta.xml new file mode 100644 index 0000000..9c4db67 --- /dev/null +++ b/src/test/goldens/async_path_test.flow-meta.xml @@ -0,0 +1,77 @@ + + + 64.0 + Default + async_path_test {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + async_update + + 0 + 0 + + NumberOfEmployees + + 100.0 + + + $Record + + + main_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + async_update + + AsyncAfterCommit + + RecordAfterSave + + Active + diff --git a/src/test/goldens/multiple_async_paths.flow-meta.xml b/src/test/goldens/multiple_async_paths.flow-meta.xml new file mode 100644 index 0000000..e2c3e75 --- /dev/null +++ b/src/test/goldens/multiple_async_paths.flow-meta.xml @@ -0,0 +1,96 @@ + + + 64.0 + Default + multiple_async_paths {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + async_update_1 + + 0 + 0 + + NumberOfEmployees + + 100.0 + + + $Record + + + async_update_2 + + 0 + 0 + + Industry + + Technology + + + $Record + + + main_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + async_update_1 + + AsyncAfterCommit + + + + async_update_2 + + AsyncAfterCommit + + RecordAfterSave + + Active + diff --git a/src/test/goldens/non_async_scheduled_path.flow-meta.xml b/src/test/goldens/non_async_scheduled_path.flow-meta.xml new file mode 100644 index 0000000..6c400b1 --- /dev/null +++ b/src/test/goldens/non_async_scheduled_path.flow-meta.xml @@ -0,0 +1,77 @@ + + + 64.0 + Default + non_async_scheduled_path {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + scheduled_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + main_update + + 0 + 0 + + Status + + Active + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + scheduled_update + + RecordField + + RecordAfterSave + + Active + From bc8d10f6cdbd9311d5e6feccf4ef8d30ec727d51 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Wed, 20 Aug 2025 16:24:00 -0500 Subject: [PATCH 3/5] Format with deno --- src/main/flow_parser.ts | 6 ++-- src/main/uml_generator.ts | 48 ++++++++++++++++++++------------ src/test/flow_parser_test.ts | 50 +++++++++++++++++++++++----------- src/test/uml_generator_test.ts | 10 +++++-- 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/main/flow_parser.ts b/src/main/flow_parser.ts index ae9faec..fdbd4cf 100644 --- a/src/main/flow_parser.ts +++ b/src/main/flow_parser.ts @@ -439,14 +439,14 @@ export class FlowParser { private getTransitionsFromFlowStart(node: flowTypes.FlowStart): Transition[] { const result: Transition[] = []; - + // Add main flow transition if (node.connector) { result.push( this.createTransition(node, node.connector, false, undefined), ); } - + // Add scheduled path transitions if (node.scheduledPaths && node.scheduledPaths.length > 0) { for (const scheduledPath of node.scheduledPaths) { @@ -458,7 +458,7 @@ export class FlowParser { } } } - + return result; } } diff --git a/src/main/uml_generator.ts b/src/main/uml_generator.ts index 155a200..a0a431f 100644 --- a/src/main/uml_generator.ts +++ b/src/main/uml_generator.ts @@ -183,79 +183,91 @@ export abstract class UmlGenerator { const start = this.parsedFlow.start; const entryCriteria: string[] = []; - + // Add process type information from the flow if (this.parsedFlow.processType) { entryCriteria.push(`Process Type: ${this.parsedFlow.processType}`); } - + // Add trigger type information if (start.triggerType) { entryCriteria.push(`Trigger Type: ${start.triggerType}`); } - + // Add object information for record-triggered flows if (start.object) { entryCriteria.push(`Object: ${start.object}`); } - + // Add record trigger type information if (start.recordTriggerType) { entryCriteria.push(`Record Trigger: ${start.recordTriggerType}`); } - + // Add entry type information if (start.entryType) { entryCriteria.push(`Entry Type: ${start.entryType}`); } - + // Add filter information if (start.filterLogic && start.filters && start.filters.length > 0) { entryCriteria.push(`Filter Logic: ${start.filterLogic}`); start.filters.forEach((filter, index) => { - entryCriteria.push(`${index + 1}. ${filter.field} ${filter.operator} ${toString(filter.value)}`); + entryCriteria.push( + `${index + 1}. ${filter.field} ${filter.operator} ${ + toString(filter.value) + }`, + ); }); } - + // Add filter formula if present if (start.filterFormula) { entryCriteria.push(`Filter Formula: ${start.filterFormula}`); } - + // Add schedule information for scheduled flows if (start.schedule) { - entryCriteria.push(`Schedule: ${start.schedule.frequency} starting ${start.schedule.startDate} at ${start.schedule.startTime}`); + entryCriteria.push( + `Schedule: ${start.schedule.frequency} starting ${start.schedule.startDate} at ${start.schedule.startTime}`, + ); } - + // Add scheduled paths information if (start.scheduledPaths && start.scheduledPaths.length > 0) { start.scheduledPaths.forEach((path, index) => { - entryCriteria.push(`Scheduled Path ${index + 1}: ${path.label} (${path.offsetNumber} ${path.offsetUnit})`); + entryCriteria.push( + `Scheduled Path ${ + index + 1 + }: ${path.label} (${path.offsetNumber} ${path.offsetUnit})`, + ); }); } - + // Add capability information if (start.capabilityTypes && start.capabilityTypes.length > 0) { start.capabilityTypes.forEach((capability, index) => { - entryCriteria.push(`Capability ${index + 1}: ${capability.capabilityName}`); + entryCriteria.push( + `Capability ${index + 1}: ${capability.capabilityName}`, + ); }); } - + // Add form information for form-triggered flows if (start.form) { entryCriteria.push(`Form: ${start.form}`); } - + // Add segment information if (start.segment) { entryCriteria.push(`Segment: ${start.segment}`); } - + // Add flow run as user information if (start.flowRunAsUser) { entryCriteria.push(`Run As: ${start.flowRunAsUser}`); } - + // If no specific criteria found, add a default message if (entryCriteria.length === 0) { entryCriteria.push("No specific entry criteria defined"); diff --git a/src/test/flow_parser_test.ts b/src/test/flow_parser_test.ts index e048167..75e006d 100644 --- a/src/test/flow_parser_test.ts +++ b/src/test/flow_parser_test.ts @@ -36,13 +36,22 @@ const TEST_FILES = { circularTransition: join(GOLDENS_PATH, "circular_transition.flow-meta.xml"), rollback: join(GOLDENS_PATH, "rollback.flow-meta.xml"), singleFilter: join(GOLDENS_PATH, "single_filter.flow-meta.xml"), - singleScheduledPath: join(GOLDENS_PATH, "single_scheduled_path.flow-meta.xml"), + singleScheduledPath: join( + GOLDENS_PATH, + "single_scheduled_path.flow-meta.xml", + ), singleCapability: join(GOLDENS_PATH, "single_capability.flow-meta.xml"), - multipleStartElements: join(GOLDENS_PATH, "multiple_start_elements.flow-meta.xml"), + multipleStartElements: join( + GOLDENS_PATH, + "multiple_start_elements.flow-meta.xml", + ), noStartElements: join(GOLDENS_PATH, "no_start_elements.flow-meta.xml"), asyncPathTest: join(GOLDENS_PATH, "async_path_test.flow-meta.xml"), multipleAsyncPaths: join(GOLDENS_PATH, "multiple_async_paths.flow-meta.xml"), - nonAsyncScheduledPath: join(GOLDENS_PATH, "non_async_scheduled_path.flow-meta.xml"), + nonAsyncScheduledPath: join( + GOLDENS_PATH, + "non_async_scheduled_path.flow-meta.xml", + ), }; const NODE_NAMES = { @@ -449,7 +458,10 @@ Deno.test("FlowParser", async (t) => { assert(parsedFlow.start.capabilityTypes); assert(Array.isArray(parsedFlow.start.capabilityTypes)); assertEquals(parsedFlow.start.capabilityTypes.length, 1); - assertEquals(parsedFlow.start.capabilityTypes[0].capabilityName, "Chatter"); + assertEquals( + parsedFlow.start.capabilityTypes[0].capabilityName, + "Chatter", + ); }, ); @@ -463,7 +475,7 @@ Deno.test("FlowParser", async (t) => { parsedFlow = await systemUnderTest.generateFlowDefinition(); assert(parsedFlow.start); - + // Check filters assert(parsedFlow.start.filters); assert(Array.isArray(parsedFlow.start.filters)); @@ -474,7 +486,7 @@ Deno.test("FlowParser", async (t) => { assertEquals(parsedFlow.start.filters[1].field, "Type"); assertEquals(parsedFlow.start.filters[1].operator, "EqualTo"); assertEquals(parsedFlow.start.filters[1].value.stringValue, "Prospect"); - + // Check scheduled paths assert(parsedFlow.start.scheduledPaths); assert(Array.isArray(parsedFlow.start.scheduledPaths)); @@ -485,13 +497,19 @@ Deno.test("FlowParser", async (t) => { assertEquals(parsedFlow.start.scheduledPaths[1].label, "Weekly"); assertEquals(parsedFlow.start.scheduledPaths[1].offsetNumber, "7"); assertEquals(parsedFlow.start.scheduledPaths[1].offsetUnit, "Days"); - + // Check capability types assert(parsedFlow.start.capabilityTypes); assert(Array.isArray(parsedFlow.start.capabilityTypes)); assertEquals(parsedFlow.start.capabilityTypes.length, 2); - assertEquals(parsedFlow.start.capabilityTypes[0].capabilityName, "Chatter"); - assertEquals(parsedFlow.start.capabilityTypes[1].capabilityName, "Lightning"); + assertEquals( + parsedFlow.start.capabilityTypes[0].capabilityName, + "Chatter", + ); + assertEquals( + parsedFlow.start.capabilityTypes[1].capabilityName, + "Lightning", + ); }, ); @@ -523,7 +541,7 @@ Deno.test("FlowParser", async (t) => { assert(parsedFlow.transitions); assertEquals(parsedFlow.transitions.length, 2); - + // Main flow transition (no label) assertEquals(parsedFlow.transitions[0], { from: START_NODE_NAME, @@ -531,7 +549,7 @@ Deno.test("FlowParser", async (t) => { fault: false, label: undefined, }); - + // Async path transition (with path type as label) assertEquals(parsedFlow.transitions[1], { from: START_NODE_NAME, @@ -553,7 +571,7 @@ Deno.test("FlowParser", async (t) => { assert(parsedFlow.transitions); assertEquals(parsedFlow.transitions.length, 3); - + // Main flow transition (no label) assertEquals(parsedFlow.transitions[0], { from: START_NODE_NAME, @@ -561,7 +579,7 @@ Deno.test("FlowParser", async (t) => { fault: false, label: undefined, }); - + // First async path transition assertEquals(parsedFlow.transitions[1], { from: START_NODE_NAME, @@ -569,7 +587,7 @@ Deno.test("FlowParser", async (t) => { fault: false, label: "AsyncAfterCommit", }); - + // Second async path transition assertEquals(parsedFlow.transitions[2], { from: START_NODE_NAME, @@ -591,7 +609,7 @@ Deno.test("FlowParser", async (t) => { assert(parsedFlow.transitions); assertEquals(parsedFlow.transitions.length, 2); - + // Main flow transition (no label) assertEquals(parsedFlow.transitions[0], { from: START_NODE_NAME, @@ -599,7 +617,7 @@ Deno.test("FlowParser", async (t) => { fault: false, label: undefined, }); - + // Non-async scheduled path transition (with path type as label) assertEquals(parsedFlow.transitions[1], { from: START_NODE_NAME, diff --git a/src/test/uml_generator_test.ts b/src/test/uml_generator_test.ts index 8128b3f..6d932a7 100644 --- a/src/test/uml_generator_test.ts +++ b/src/test/uml_generator_test.ts @@ -700,7 +700,8 @@ Deno.test("UmlGenerator", async (t) => { value: { stringValue: "Inactive" }, }, ], - filterFormula: "{!$Record.Type} = 'Customer' && {!$Record.Status} != 'Inactive'", + filterFormula: + "{!$Record.Type} = 'Customer' && {!$Record.Status} != 'Inactive'", }; const uml = systemUnderTest.generateUml(); @@ -753,7 +754,8 @@ Deno.test("UmlGenerator", async (t) => { connector: { targetReference: "END", isGoTo: false }, offsetNumber: "1", offsetUnit: flowTypes.FlowScheduledPathOffsetUnit.DAYS, - timeSource: flowTypes.FlowScheduledPathTimeSource.RECORD_TRIGGER_EVENT, + timeSource: + flowTypes.FlowScheduledPathTimeSource.RECORD_TRIGGER_EVENT, description: "Daily scheduled path", }, { @@ -1052,7 +1054,9 @@ Deno.test("UmlGenerator", async (t) => { } } - const recordChangeGenerator = new ConcreteUmlGenerator(recordChangeMockFlow); + const recordChangeGenerator = new ConcreteUmlGenerator( + recordChangeMockFlow, + ); const uml = recordChangeGenerator.generateUml(); const expectedContent = [ From 85d737e8fdfd8ead8b770ba3a138504a3d1800cb Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Wed, 20 Aug 2025 20:05:37 -0500 Subject: [PATCH 4/5] Format with latest version of Deno --- .../goldens/async_path_test.flow-meta.xml | 148 +++---- .../goldens/circular_transition.flow-meta.xml | 2 +- .../missing_transition_node.flow-meta.xml | 2 +- .../multiple_async_paths.flow-meta.xml | 186 ++++---- .../goldens/multiple_elements.flow-meta.xml | 6 +- src/test/goldens/no_start_node.flow-meta.xml | 3 +- .../non_async_scheduled_path.flow-meta.xml | 149 +++---- src/test/goldens/rollback.flow-meta.xml | 4 +- src/test/goldens/sample.flow-meta.xml | 416 +++++++++--------- .../goldens/single_elements.flow-meta.xml | 2 +- 10 files changed, 461 insertions(+), 457 deletions(-) diff --git a/src/test/goldens/async_path_test.flow-meta.xml b/src/test/goldens/async_path_test.flow-meta.xml index 9c4db67..375b68c 100644 --- a/src/test/goldens/async_path_test.flow-meta.xml +++ b/src/test/goldens/async_path_test.flow-meta.xml @@ -1,77 +1,77 @@ - 64.0 - Default - async_path_test {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - - CanvasMode - - AUTO_LAYOUT_CANVAS - - - - OriginBuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - async_update - - 0 - 0 - - NumberOfEmployees - - 100.0 - - - $Record - - - main_update - - 0 - 0 - - Type - - Prospect - - - $Record - - - 0 - 0 - - main_update - - and - - Description - IsNull - - false - - - Account - Create - - - async_update - - AsyncAfterCommit - - RecordAfterSave - - Active + 64.0 + Default + async_path_test {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + async_update + + 0 + 0 + + NumberOfEmployees + + 100.0 + + + $Record + + + main_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + async_update + + AsyncAfterCommit + + RecordAfterSave + + Active diff --git a/src/test/goldens/circular_transition.flow-meta.xml b/src/test/goldens/circular_transition.flow-meta.xml index 84ec7e1..16c8706 100644 --- a/src/test/goldens/circular_transition.flow-meta.xml +++ b/src/test/goldens/circular_transition.flow-meta.xml @@ -29,4 +29,4 @@ myLoop - \ No newline at end of file + diff --git a/src/test/goldens/missing_transition_node.flow-meta.xml b/src/test/goldens/missing_transition_node.flow-meta.xml index ae1e64c..c570fa3 100644 --- a/src/test/goldens/missing_transition_node.flow-meta.xml +++ b/src/test/goldens/missing_transition_node.flow-meta.xml @@ -23,4 +23,4 @@ Non_Existing_Element - \ No newline at end of file + diff --git a/src/test/goldens/multiple_async_paths.flow-meta.xml b/src/test/goldens/multiple_async_paths.flow-meta.xml index e2c3e75..df5900e 100644 --- a/src/test/goldens/multiple_async_paths.flow-meta.xml +++ b/src/test/goldens/multiple_async_paths.flow-meta.xml @@ -1,96 +1,96 @@ - 64.0 - Default - multiple_async_paths {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - - CanvasMode - - AUTO_LAYOUT_CANVAS - - - - OriginBuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - async_update_1 - - 0 - 0 - - NumberOfEmployees - - 100.0 - - - $Record - - - async_update_2 - - 0 - 0 - - Industry - - Technology - - - $Record - - - main_update - - 0 - 0 - - Type - - Prospect - - - $Record - - - 0 - 0 - - main_update - - and - - Description - IsNull - - false - - - Account - Create - - - async_update_1 - - AsyncAfterCommit - - - - async_update_2 - - AsyncAfterCommit - - RecordAfterSave - - Active + 64.0 + Default + multiple_async_paths {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + async_update_1 + + 0 + 0 + + NumberOfEmployees + + 100.0 + + + $Record + + + async_update_2 + + 0 + 0 + + Industry + + Technology + + + $Record + + + main_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + async_update_1 + + AsyncAfterCommit + + + + async_update_2 + + AsyncAfterCommit + + RecordAfterSave + + Active diff --git a/src/test/goldens/multiple_elements.flow-meta.xml b/src/test/goldens/multiple_elements.flow-meta.xml index a333786..7ed6600 100644 --- a/src/test/goldens/multiple_elements.flow-meta.xml +++ b/src/test/goldens/multiple_elements.flow-meta.xml @@ -68,7 +68,7 @@ myRecordDelete - + myRecordDelete2 @@ -80,7 +80,7 @@ myRecordRollback - + myRecordRollback2 @@ -125,4 +125,4 @@ myActionCall2 - \ No newline at end of file + diff --git a/src/test/goldens/no_start_node.flow-meta.xml b/src/test/goldens/no_start_node.flow-meta.xml index 0179010..d932858 100644 --- a/src/test/goldens/no_start_node.flow-meta.xml +++ b/src/test/goldens/no_start_node.flow-meta.xml @@ -15,5 +15,4 @@ limitations under the License. --> - - \ No newline at end of file + diff --git a/src/test/goldens/non_async_scheduled_path.flow-meta.xml b/src/test/goldens/non_async_scheduled_path.flow-meta.xml index 6c400b1..47c90c6 100644 --- a/src/test/goldens/non_async_scheduled_path.flow-meta.xml +++ b/src/test/goldens/non_async_scheduled_path.flow-meta.xml @@ -1,77 +1,78 @@ - 64.0 - Default - non_async_scheduled_path {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - - CanvasMode - - AUTO_LAYOUT_CANVAS - - - - OriginBuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - scheduled_update - - 0 - 0 - - Type - - Prospect - - - $Record - - - main_update - - 0 - 0 - - Status - - Active - - - $Record - - - 0 - 0 - - main_update - - and - - Description - IsNull - - false - - - Account - Create - - - scheduled_update - - RecordField - - RecordAfterSave - - Active + 64.0 + Default + non_async_scheduled_path {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + scheduled_update + + 0 + 0 + + Type + + Prospect + + + $Record + + + main_update + + 0 + 0 + + Status + + Active + + + $Record + + + 0 + 0 + + main_update + + and + + Description + IsNull + + false + + + Account + Create + + + scheduled_update + + RecordField + + RecordAfterSave + + Active diff --git a/src/test/goldens/rollback.flow-meta.xml b/src/test/goldens/rollback.flow-meta.xml index 06f3270..b5698ec 100644 --- a/src/test/goldens/rollback.flow-meta.xml +++ b/src/test/goldens/rollback.flow-meta.xml @@ -26,7 +26,7 @@ myRecordLookup - myRecordRollback + myRecordRollback @@ -38,4 +38,4 @@ myScreen - \ No newline at end of file + diff --git a/src/test/goldens/sample.flow-meta.xml b/src/test/goldens/sample.flow-meta.xml index 066a821..665b7e8 100644 --- a/src/test/goldens/sample.flow-meta.xml +++ b/src/test/goldens/sample.flow-meta.xml @@ -16,209 +16,213 @@ --> - - Add_Issue_Inserting_Tag_Record_Error - - 578 - 566 - TriggerActionFlowAddError - apex - - T__record - Case - - CurrentTransaction - - errorMessage - - issueInsertingTagRecordError - - - - record - - record - - - TriggerActionFlowAddError - 1 - - - Add_No_Tag_Definition_Found_Error - - 50 - 350 - TriggerActionFlowAddError - apex - - T__record - Case - - CurrentTransaction - - errorMessage - - noTagDefinitionFoundError - - - - record - - record - - - TriggerActionFlowAddError - 1 - - 59.0 - - Populate_Tag - - 314 - 350 - - tagToInsert.Case__c - Assign - - record.Id - - - - tagToInsert.Record_Id__c - Assign - - record.Id - - - - tagToInsert.Tag_Definition__c - Assign - - Get_Aurora_Tag_Definition.Id - - - - Insert_Tag - - - - Was_Tag_Definition_c_found - - 182 - 242 - - Populate_Tag - - Yes - - No - and - - Get_Aurora_Tag_Definition - IsNull - - true - - - - Add_No_Tag_Definition_Found_Error - - - - - Automatically inserts a Tag__c related to the Aurora Tag_Definition__c for newly created cases. - Default - - issueInsertingTagRecordError - String - "The system encountered an error when trying to insert the Tag__c record for this case which is related to the " +{!$Label.Aurora_Tag_Definition_Name} + " Tag_Definition__c." - - - noTagDefinitionFoundError - String - "The system was unable to find the " + {!$Label.Aurora_Tag_Definition_Name} + " Tag_Definition__c record." - - TA_Case_InsertAuroraTag {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - - CanvasMode - - AUTO_LAYOUT_CANVAS - - - - OriginBuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - Insert_Tag - - 314 - 458 - - Add_Issue_Inserting_Tag_Record_Error - - tagToInsert - - - Get_Aurora_Tag_Definition - - 182 - 134 - false - - Was_Tag_Definition_c_found - - and - - Type__c - EqualTo - - General Tag - - - - Name - EqualTo - - $Label.Aurora_Tag_Definition_Name - - - true - Tag_Definition__c - true - - - 56 - 0 - - Get_Aurora_Tag_Definition - - - Active - - record - SObject - false - true - true - Case - - - tagToInsert - SObject - false - false - false - Tag__c - - \ No newline at end of file + + Add_Issue_Inserting_Tag_Record_Error + + 578 + 566 + TriggerActionFlowAddError + apex + + T__record + Case + + CurrentTransaction + + errorMessage + + issueInsertingTagRecordError + + + + record + + record + + + TriggerActionFlowAddError + 1 + + + Add_No_Tag_Definition_Found_Error + + 50 + 350 + TriggerActionFlowAddError + apex + + T__record + Case + + CurrentTransaction + + errorMessage + + noTagDefinitionFoundError + + + + record + + record + + + TriggerActionFlowAddError + 1 + + 59.0 + + Populate_Tag + + 314 + 350 + + tagToInsert.Case__c + Assign + + record.Id + + + + tagToInsert.Record_Id__c + Assign + + record.Id + + + + tagToInsert.Tag_Definition__c + Assign + + Get_Aurora_Tag_Definition.Id + + + + Insert_Tag + + + + Was_Tag_Definition_c_found + + 182 + 242 + + Populate_Tag + + Yes + + No + and + + Get_Aurora_Tag_Definition + IsNull + + true + + + + Add_No_Tag_Definition_Found_Error + + + + + Automatically inserts a Tag__c related to the Aurora Tag_Definition__c for newly created cases. + Default + + issueInsertingTagRecordError + String + "The system encountered an error when trying to insert the Tag__c record for this case which is related to the " +{!$Label.Aurora_Tag_Definition_Name} + " Tag_Definition__c." + + + noTagDefinitionFoundError + String + "The system was unable to find the " + {!$Label.Aurora_Tag_Definition_Name} + " Tag_Definition__c record." + + TA_Case_InsertAuroraTag {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + Insert_Tag + + 314 + 458 + + Add_Issue_Inserting_Tag_Record_Error + + tagToInsert + + + Get_Aurora_Tag_Definition + + 182 + 134 + false + + Was_Tag_Definition_c_found + + and + + Type__c + EqualTo + + General Tag + + + + Name + EqualTo + + $Label.Aurora_Tag_Definition_Name + + + true + Tag_Definition__c + true + + + 56 + 0 + + Get_Aurora_Tag_Definition + + + Active + + record + SObject + false + true + true + Case + + + tagToInsert + SObject + false + false + false + Tag__c + + diff --git a/src/test/goldens/single_elements.flow-meta.xml b/src/test/goldens/single_elements.flow-meta.xml index 49fb903..0a93a7b 100644 --- a/src/test/goldens/single_elements.flow-meta.xml +++ b/src/test/goldens/single_elements.flow-meta.xml @@ -74,4 +74,4 @@ myActionCall - \ No newline at end of file + From 4a02f7b6e8222ac08b0ed645761e4572d7d1b695 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Wed, 20 Aug 2025 20:19:31 -0500 Subject: [PATCH 5/5] Remove scheduled paths information from UmlGenerator and related tests to streamline UML generation output. --- src/main/uml_generator.ts | 11 ----------- src/test/uml_generator_test.ts | 2 -- 2 files changed, 13 deletions(-) diff --git a/src/main/uml_generator.ts b/src/main/uml_generator.ts index a0a431f..d3ea1df 100644 --- a/src/main/uml_generator.ts +++ b/src/main/uml_generator.ts @@ -233,17 +233,6 @@ export abstract class UmlGenerator { ); } - // Add scheduled paths information - if (start.scheduledPaths && start.scheduledPaths.length > 0) { - start.scheduledPaths.forEach((path, index) => { - entryCriteria.push( - `Scheduled Path ${ - index + 1 - }: ${path.label} (${path.offsetNumber} ${path.offsetUnit})`, - ); - }); - } - // Add capability information if (start.capabilityTypes && start.capabilityTypes.length > 0) { start.capabilityTypes.forEach((capability, index) => { diff --git a/src/test/uml_generator_test.ts b/src/test/uml_generator_test.ts index 6d932a7..926d85f 100644 --- a/src/test/uml_generator_test.ts +++ b/src/test/uml_generator_test.ts @@ -776,8 +776,6 @@ Deno.test("UmlGenerator", async (t) => { "Process Type: AutoLaunchedFlow", "Trigger Type: Scheduled", "Schedule: Daily starting 2024-01-01 at 09:00:00", - "Scheduled Path 1: Daily Processing (1 Days)", - "Scheduled Path 2: Weekly Report (7 Days)", ]; expectedContent.forEach((content) => {