From 9517567262e678b2d24d36f1674291a11a8fb159 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Fri, 6 Mar 2026 14:50:37 +0100 Subject: [PATCH 1/4] test: verify `usedBy` attaches to correct scope --- .../fixtures/zeebe/used-variables.scopes.bpmn | 73 +++++++++++++++++++ test/spec/zeebe/ZeebeVariableResolver.spec.js | 47 ++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 test/fixtures/zeebe/used-variables.scopes.bpmn diff --git a/test/fixtures/zeebe/used-variables.scopes.bpmn b/test/fixtures/zeebe/used-variables.scopes.bpmn new file mode 100644 index 0000000..e5da4a0 --- /dev/null +++ b/test/fixtures/zeebe/used-variables.scopes.bpmn @@ -0,0 +1,73 @@ + + + + + + + + + + Flow_03u12aa + + + + + + + + + + + + Flow_03u12aa + + + Defines <approved> as local variable + + + + Uses <approved> variable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/zeebe/ZeebeVariableResolver.spec.js b/test/spec/zeebe/ZeebeVariableResolver.spec.js index f6e3536..d2f3ce3 100644 --- a/test/spec/zeebe/ZeebeVariableResolver.spec.js +++ b/test/spec/zeebe/ZeebeVariableResolver.spec.js @@ -31,6 +31,7 @@ import subprocessNoOutputMappingXML from 'test/fixtures/zeebe/sub-process.no-out import longBrokenExpressionXML from 'test/fixtures/zeebe/long-broken-expression.bpmn'; import immediatelyBrokenExpressionXML from 'test/fixtures/zeebe/immediately-broken-expression.bpmn'; import typeResolutionXML from 'test/fixtures/zeebe/type-resolution.bpmn'; +import usedVariablesScopesXML from 'test/fixtures/zeebe/used-variables.scopes.bpmn'; import VariableProvider from 'lib/VariableProvider'; import { getInputOutput } from '../../../lib/base/util/ExtensionElementsUtil'; @@ -2602,6 +2603,52 @@ describe('ZeebeVariableResolver', function() { }); + + describe('used variables - scopes', function() { + + beforeEach(bootstrapModeler(usedVariablesScopesXML, { + additionalModules: [ + ZeebeVariableResolverModule + ], + moddleExtensions: { + zeebe: ZeebeModdle + } + })); + + + it('should attach to local scope', inject(async function(elementRegistry, variableResolver) { + + // given + const subProcess = elementRegistry.get('SubProcess_1'); + + // when + const variables = await variableResolver.getVariablesForElement(subProcess); + + // then + expect(variables).to.variableEqual([ + { name: 'taskResult' }, + { name: 'approved', usedBy: [ 'Task_2' ] } + ]); + })); + + + it('should attach to global scope', inject(async function(elementRegistry, variableResolver) { + + // given + const rootElement = elementRegistry.get('Process_1'); + + // when + const variables = await variableResolver.getVariablesForElement(rootElement); + + // then + expect(variables).to.variableEqual([ + { name: 'taskResult' }, + { name: 'approved', usedBy: [ 'Task_1' ] } + ]); + })); + + }); + }); // helpers ////////////////////// From ace906e5c6a1fd3410de3005a0773901cd64157b Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Fri, 6 Mar 2026 14:39:35 +0100 Subject: [PATCH 2/4] test: validate dual use of read and written variable --- test/fixtures/zeebe/read-write.bpmn | 45 ++++++++++++++ .../zeebe/read-write.hierarchical.bpmn | 45 ++++++++++++++ test/spec/zeebe/ZeebeVariableResolver.spec.js | 62 +++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 test/fixtures/zeebe/read-write.bpmn create mode 100644 test/fixtures/zeebe/read-write.hierarchical.bpmn diff --git a/test/fixtures/zeebe/read-write.bpmn b/test/fixtures/zeebe/read-write.bpmn new file mode 100644 index 0000000..6e7a31b --- /dev/null +++ b/test/fixtures/zeebe/read-write.bpmn @@ -0,0 +1,45 @@ + + + + + + + + + + + + + Write: approved + + + + Read: approved + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/zeebe/read-write.hierarchical.bpmn b/test/fixtures/zeebe/read-write.hierarchical.bpmn new file mode 100644 index 0000000..586a784 --- /dev/null +++ b/test/fixtures/zeebe/read-write.hierarchical.bpmn @@ -0,0 +1,45 @@ + + + + + + + + + + + + + Write: application.approved + + + + Read: application.approved + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/zeebe/ZeebeVariableResolver.spec.js b/test/spec/zeebe/ZeebeVariableResolver.spec.js index d2f3ce3..dd6028f 100644 --- a/test/spec/zeebe/ZeebeVariableResolver.spec.js +++ b/test/spec/zeebe/ZeebeVariableResolver.spec.js @@ -32,6 +32,8 @@ import longBrokenExpressionXML from 'test/fixtures/zeebe/long-broken-expression. import immediatelyBrokenExpressionXML from 'test/fixtures/zeebe/immediately-broken-expression.bpmn'; import typeResolutionXML from 'test/fixtures/zeebe/type-resolution.bpmn'; import usedVariablesScopesXML from 'test/fixtures/zeebe/used-variables.scopes.bpmn'; +import readWriteXML from 'test/fixtures/zeebe/read-write.bpmn'; +import readWriteHierarchicalXML from 'test/fixtures/zeebe/read-write.hierarchical.bpmn'; import VariableProvider from 'lib/VariableProvider'; import { getInputOutput } from '../../../lib/base/util/ExtensionElementsUtil'; @@ -2649,6 +2651,66 @@ describe('ZeebeVariableResolver', function() { }); + + describe('used variables - read and written', function() { + + beforeEach(bootstrapModeler(readWriteXML, { + additionalModules: [ + ZeebeVariableResolverModule + ], + moddleExtensions: { + zeebe: ZeebeModdle + } + })); + + + it('should indicate dual use', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('ValidateApprovedTask'); + + // when + const variables = await variableResolver.getVariablesForElement(task); + + // then + expect(variables).to.variableEqual([ + { name: 'approved', scope: 'Process_1', origin: [ 'ValidateApprovedTask' ], usedBy: [ 'ValidateApprovedTask' ] }, + { name: 'localApproved', scope: 'ValidateApprovedTask' } + ]); + })); + + }); + + + describe('used variables - read and written / hierarchical', function() { + + beforeEach(bootstrapModeler(readWriteHierarchicalXML, { + additionalModules: [ + ZeebeVariableResolverModule + ], + moddleExtensions: { + zeebe: ZeebeModdle + } + })); + + + it('should indicate dual use', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('ValidateApprovedTask'); + + // when + const variables = await variableResolver.getVariablesForElement(task); + + // then + expect(variables).to.variableEqual([ + { name: 'application', scope: 'Process_1', origin: [ 'ValidateApprovedTask' ], usedBy: [ 'ValidateApprovedTask' ] }, + { name: 'localApproved', scope: 'ValidateApprovedTask' } + ]); + })); + + }); + }); // helpers ////////////////////// From 2ee9aea7dad4be1a9cf5c73dcbf1e66139c0de5c Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Fri, 6 Mar 2026 13:52:02 +0100 Subject: [PATCH 3/4] test: verify used variables in pro-code setup --- test/fixtures/zeebe/used-variables.bpmn | 178 ++++++++++++++++++ test/spec/zeebe/ZeebeVariableResolver.spec.js | 83 ++++++++ 2 files changed, 261 insertions(+) create mode 100644 test/fixtures/zeebe/used-variables.bpmn diff --git a/test/fixtures/zeebe/used-variables.bpmn b/test/fixtures/zeebe/used-variables.bpmn new file mode 100644 index 0000000..63face7 --- /dev/null +++ b/test/fixtures/zeebe/used-variables.bpmn @@ -0,0 +1,178 @@ + + + + + + SequenceFlow_2 + + + + SequenceFlow_2 + SequenceFlow_3 + + + SequenceFlow_3 + SequenceFlow_1 + SequenceFlow_4 + + + + =isAgeVerified + + + + SequenceFlow_4 + SequenceFlow_5 + + + SequenceFlow_1 + SequenceFlow_5 + SequenceFlow_6 + + + + + SequenceFlow_6 + SequenceFlow_10 + SequenceFlow_7 + + + =isAgeVerified and is empty(checkResults) + + + + SequenceFlow_10 + SequenceFlow_9 + + + SequenceFlow_9 + + + + SequenceFlow_7 + SequenceFlow_8 + + + SequenceFlow_8 + + + + This process models a typical pro-code scenario where the data model is implicit ("in code") + +* variables are only used, never written in the model +* intelligence should indicate variables used + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/zeebe/ZeebeVariableResolver.spec.js b/test/spec/zeebe/ZeebeVariableResolver.spec.js index dd6028f..2a24175 100644 --- a/test/spec/zeebe/ZeebeVariableResolver.spec.js +++ b/test/spec/zeebe/ZeebeVariableResolver.spec.js @@ -31,6 +31,7 @@ import subprocessNoOutputMappingXML from 'test/fixtures/zeebe/sub-process.no-out import longBrokenExpressionXML from 'test/fixtures/zeebe/long-broken-expression.bpmn'; import immediatelyBrokenExpressionXML from 'test/fixtures/zeebe/immediately-broken-expression.bpmn'; import typeResolutionXML from 'test/fixtures/zeebe/type-resolution.bpmn'; +import usedVariablesXML from 'test/fixtures/zeebe/used-variables.bpmn'; import usedVariablesScopesXML from 'test/fixtures/zeebe/used-variables.scopes.bpmn'; import readWriteXML from 'test/fixtures/zeebe/read-write.bpmn'; import readWriteHierarchicalXML from 'test/fixtures/zeebe/read-write.hierarchical.bpmn'; @@ -2652,6 +2653,88 @@ describe('ZeebeVariableResolver', function() { }); + describe('used variables - pro code', function() { + + beforeEach(bootstrapModeler(usedVariablesXML, { + additionalModules: [ + ZeebeVariableResolverModule + ], + moddleExtensions: { + zeebe: ZeebeModdle + } + })); + + + it('should expose used variables globally', inject(async function(elementRegistry, variableResolver) { + + // when + const allVariables = await variableResolver.getVariables(); + + // then + expect(allVariables).to.have.property('Process_1'); + + expect(allVariables['Process_1']).to.variableEqual([ + { name: 'isAgeVerified', usedBy: [ 'SequenceFlow_1', 'SequenceFlow_10' ], scope: undefined, origin: undefined }, + { name: 'subscriptionRequestID', usedBy: [ 'VerifyAgeTask' ], scope: undefined, origin: undefined }, + { name: 'checkResults', usedBy: [ 'SequenceFlow_10' ], scope: undefined, origin: undefined } + ]); + + // and when + const rootElement = elementRegistry.get('Process_1'); + + const processVariables = await variableResolver.getProcessVariables(rootElement); + + expect(processVariables).to.eql(allVariables['Process_1']); + })); + + + describe('should expose used variables per element', function() { + + it('sequence flow', inject(async function(elementRegistry, variableResolver) { + + // when + const flow = elementRegistry.get('SequenceFlow_1'); + + const variables = await variableResolver.getVariablesForElement(flow); + + // then + expect(variables).to.variableEqual([ + { name: 'isAgeVerified', usedBy: [ 'SequenceFlow_1', 'SequenceFlow_10' ], scope: undefined, origin: undefined } + ]); + })); + + + it('receive task', inject(async function(elementRegistry, variableResolver) { + + // when + const task = elementRegistry.get('VerifyAgeTask'); + + const variables = await variableResolver.getVariablesForElement(task); + + // then + expect(variables).to.variableEqual([ + { name: 'subscriptionRequestID', usedBy: [ 'VerifyAgeTask' ], scope: undefined, origin: undefined } + ]); + })); + + + it('process', inject(async function(elementRegistry, variableResolver) { + + // when + const rootElement = elementRegistry.get('Process_1'); + + const variables = await variableResolver.getVariablesForElement(rootElement); + + // then + // TODO(nikku): should that include variables not used by process? + expect(variables).to.variableEqual([]); + })); + + }); + + }); + + describe('used variables - read and written', function() { beforeEach(bootstrapModeler(readWriteXML, { From c5e1172f88d5aacc6e82f90a3f5a201dee60a92d Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Sat, 7 Mar 2026 09:33:34 +0100 Subject: [PATCH 4/4] test: verify filtering of variables works --- test/fixtures/zeebe/filtering.bpmn | 82 +++++++++++ test/spec/zeebe/ZeebeVariableResolver.spec.js | 131 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 test/fixtures/zeebe/filtering.bpmn diff --git a/test/fixtures/zeebe/filtering.bpmn b/test/fixtures/zeebe/filtering.bpmn new file mode 100644 index 0000000..3660321 --- /dev/null +++ b/test/fixtures/zeebe/filtering.bpmn @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + declare: subFoo = globalFoo + + + + declare: taskFoo = subFoo +read: taskFoo + + + + write: localResult (-> depending on taskFoo) + + + + produce: taskResult = localResult + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/zeebe/ZeebeVariableResolver.spec.js b/test/spec/zeebe/ZeebeVariableResolver.spec.js index 2a24175..541108c 100644 --- a/test/spec/zeebe/ZeebeVariableResolver.spec.js +++ b/test/spec/zeebe/ZeebeVariableResolver.spec.js @@ -35,6 +35,7 @@ import usedVariablesXML from 'test/fixtures/zeebe/used-variables.bpmn'; import usedVariablesScopesXML from 'test/fixtures/zeebe/used-variables.scopes.bpmn'; import readWriteXML from 'test/fixtures/zeebe/read-write.bpmn'; import readWriteHierarchicalXML from 'test/fixtures/zeebe/read-write.hierarchical.bpmn'; +import filteringXML from 'test/fixtures/zeebe/filtering.bpmn'; import VariableProvider from 'lib/VariableProvider'; import { getInputOutput } from '../../../lib/base/util/ExtensionElementsUtil'; @@ -2794,6 +2795,136 @@ describe('ZeebeVariableResolver', function() { }); + + describe('filtering', function() { + + beforeEach(bootstrapModeler(filteringXML, { + additionalModules: [ + ZeebeVariableResolverModule + ], + moddleExtensions: { + zeebe: ZeebeModdle + } + })); + + + + it('should NOT filter', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task); + + // then + expect(variables).to.variableEqual([ + { name: 'globalFoo', scope: undefined, usedBy: [ 'SubProcess_1' ] }, + { name: 'subFoo', scope: 'SubProcess_1', origin: [ 'SubProcess_1' ], usedBy: [ 'Task_1' ] }, + { name: 'taskFoo', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'localResult', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'taskResult', scope: 'Process_1', origin: [ 'Task_1' ] } + ]); + })); + + + it('should filter read', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { read: true }); + + // then + expect(variables).to.variableEqual([ + { name: 'subFoo', scope: 'SubProcess_1', origin: [ 'SubProcess_1' ], usedBy: [ 'Task_1' ] }, + { name: 'taskFoo', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'localResult', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] } + ]); + })); + + + it('should filter read / local', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { read: true, local: true }); + + // then + expect(variables).to.variableEqual([ + { name: 'taskFoo', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'localResult', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] } + ]); + })); + + + it('should filter read / global', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { read: true, local: false }); + + // then + expect(variables).to.variableEqual([ + { name: 'subFoo', scope: 'SubProcess_1', origin: [ 'SubProcess_1' ], usedBy: [ 'Task_1' ] } + ]); + })); + + + it('should filter write', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { written: true }); + + // then + expect(variables).to.variableEqual([ + { name: 'taskFoo', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'localResult', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'taskResult', scope: 'Process_1', origin: [ 'Task_1' ] } + ]); + })); + + + it('should filter written / local', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { written: true, local: true }); + + // then + expect(variables).to.variableEqual([ + { name: 'taskFoo', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] }, + { name: 'localResult', scope: 'Task_1', origin: [ 'Task_1' ], usedBy: [ 'Task_1' ] } + ]); + })); + + + it('should filter written / global', inject(async function(elementRegistry, variableResolver) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + const variables = await variableResolver.getVariablesForElement(task, { written: true, local: false }); + + // then + expect(variables).to.variableEqual([ + { name: 'taskResult', scope: 'Process_1', origin: [ 'Task_1' ] } + ]); + })); + + }); + }); // helpers //////////////////////