diff --git a/.eslintrc.json b/.eslintrc.json
index 7417086..931d2bd 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -7,7 +7,8 @@
"shared-node-browser": true,
"es6": true
},
- "extends": "eslint:recommended",
+ "plugins": ["import"],
+ "extends": ["eslint:recommended", "plugin:import/recommended"],
"rules": {
"brace-style": [2, "1tbs", {
"allowSingleLine": false
@@ -37,6 +38,7 @@
"eol-last": 2,
"eqeqeq": 2,
"handle-callback-err": 2,
+ "import/extensions": [2, "always"],
"indent": [2, 2, {
"SwitchCase": 1
}],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9445e50..bbc24d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
Changelog
=========
+# 6.0.0
+
+- Require peer dependency `bpmn-elements >= 8`
+- Support timer event definition cron time cycle by overloading parse function. Requires `bpmn-elements >= 10`
+
# 5.0.2
- Correct package module file...
diff --git a/index.d.ts b/index.d.ts
index 602998a..87deab3 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,8 +1,11 @@
declare module '@onify/flow-extensions' {
- import { SequenceFlow, ElementBase, Context, IExtension } from 'bpmn-elements';
+ import { SequenceFlow, TimerEventDefinition, ElementBase, Context, IExtension } from 'bpmn-elements';
import { extendFn as extendFunction } from 'moddle-context-serializer';
export class OnifySequenceFlow extends SequenceFlow {}
+ export class OnifyTimerEventDefinition extends TimerEventDefinition {
+ readonly supports: string[];
+ }
export function extensions(element: ElementBase, context: Context): IExtension;
export const extendFn: extendFunction;
}
diff --git a/package.json b/package.json
index 487d9cd..0664c7b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@onify/flow-extensions",
- "version": "5.0.2",
+ "version": "6.0.0",
"description": "Onify Flow extensions",
"type": "module",
"module": "src/index.js",
@@ -38,26 +38,30 @@
"license": "MIT",
"devDependencies": {
"@aircall/expression-parser": "^1.0.4",
- "@babel/cli": "^7.21.5",
- "@babel/core": "^7.21.8",
- "@babel/preset-env": "^7.21.5",
- "@babel/register": "^7.21.0",
+ "@babel/cli": "^7.23.4",
+ "@babel/core": "^7.23.5",
+ "@babel/preset-env": "^7.23.5",
+ "@babel/register": "^7.22.15",
"bpmn-elements-8-1": "npm:bpmn-elements@8.1",
- "bpmn-engine": "^16.0.0",
+ "bpmn-engine": "^17.1.1",
"bpmn-engine-14": "npm:bpmn-engine@14",
"bpmn-moddle": "^8.0.1",
- "c8": "^7.13.0",
+ "c8": "^8.0.1",
"camunda-bpmn-moddle": "^7.0.1",
- "chai": "^4.3.7",
- "chronokinesis": "^5.0.2",
+ "chai": "^4.3.10",
+ "chronokinesis": "^6.0.0",
"debug": "^4.3.4",
- "eslint": "^8.39.0",
+ "eslint": "^8.55.0",
+ "eslint-plugin-import": "^2.29.0",
"mocha": "^10.2.0",
"mocha-cakes-2": "^3.3.0",
- "moddle-context-serializer": "^3.2.2"
+ "moddle-context-serializer": "^4.1.1"
+ },
+ "peerDependencies": {
+ "bpmn-elements": ">=8"
},
"dependencies": {
- "cron-parser": "^4.8.1"
+ "cron-parser": "^4.9.0"
},
"files": [
"src",
diff --git a/src/OnifyTimerEventDefinition.js b/src/OnifyTimerEventDefinition.js
new file mode 100644
index 0000000..6ef0f66
--- /dev/null
+++ b/src/OnifyTimerEventDefinition.js
@@ -0,0 +1,25 @@
+import { TimerEventDefinition } from 'bpmn-elements';
+import cronParser from 'cron-parser';
+
+export class OnifyTimerEventDefinition extends TimerEventDefinition {
+ constructor(activity, def) {
+ super(activity, def);
+ Object.defineProperty(this, 'supports', {
+ value: ['cron'],
+ });
+ }
+ parse(timerType, value) {
+ let cron;
+ if (timerType === 'timeCycle' && (cron = cronParser.parseString(value))) {
+ if (cron.expressions?.length) {
+ // cronParser.parseString expressions disregards seconds, so we have to parse again
+ const expireAt = cronParser.parseExpression(value).next().toDate();
+ return {
+ expireAt,
+ delay: expireAt - Date.now(),
+ };
+ }
+ }
+ return super.parse(timerType, value);
+ }
+}
diff --git a/src/formatters.js b/src/formatters.js
index cd908b2..8abd724 100755
--- a/src/formatters.js
+++ b/src/formatters.js
@@ -10,6 +10,7 @@ export class FormatActivity {
let timeCycles;
if (activity.eventDefinitions) {
for (const ed of activity.eventDefinitions.filter((e) => e.type === 'bpmn:TimerEventDefinition')) {
+ if (ed.supports?.includes('cron')) continue;
if (!('timeCycle' in ed)) continue;
timeCycles = timeCycles || [];
timeCycles.push(ed.timeCycle);
diff --git a/src/index.js b/src/index.js
index a52964d..723addc 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,19 +1,14 @@
-import {OnifyProcessExtensions} from './OnifyProcessExtensions.js';
-import {OnifyElementExtensions} from './OnifyElementExtensions.js';
-import {OnifySequenceFlow} from './OnifySequenceFlow.js';
+import { OnifyProcessExtensions } from './OnifyProcessExtensions.js';
+import { OnifyElementExtensions } from './OnifyElementExtensions.js';
+export { OnifySequenceFlow } from './OnifySequenceFlow.js';
+export { OnifyTimerEventDefinition } from './OnifyTimerEventDefinition.js';
-export {
- extensions,
- extendFn,
- OnifySequenceFlow,
-};
-
-function extensions(element, context) {
+export function extensions(element, context) {
if (element.type === 'bpmn:Process') return new OnifyProcessExtensions(element, context);
return new OnifyElementExtensions(element, context);
}
-function extendFn(behaviour, context) {
+export function extendFn(behaviour, context) {
if (behaviour.$type === 'bpmn:StartEvent' && behaviour.eventDefinitions) {
const timer = behaviour.eventDefinitions.find(({type, behaviour: edBehaviour}) => edBehaviour && type === 'bpmn:TimerEventDefinition');
if (timer && timer.behaviour.timeCycle) Object.assign(behaviour, {scheduledStart: timer.behaviour.timeCycle});
diff --git a/test/extensions-test.js b/test/extensions-test.js
index e3b745c..4dd4c92 100644
--- a/test/extensions-test.js
+++ b/test/extensions-test.js
@@ -1,5 +1,5 @@
-import {default as Serializer, TypeResolver} from 'moddle-context-serializer';
-import {extendFn} from '../src/index.js';
+import {Serializer, TypeResolver} from 'moddle-context-serializer';
+import * as flowExtensions from '../src/index.js';
import * as Elements from 'bpmn-elements';
import factory from './helpers/factory.js';
import testHelpers from './helpers/testHelpers.js';
@@ -10,90 +10,66 @@ describe('extensions', () => {
moddleExtensions = await testHelpers.getModdleExtensions();
});
- it('extendFn registers scripts', async () => {
- const source = factory.resource('activedirectory-index-users.bpmn');
- const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
- const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn);
-
- expect(serialized.elements.scripts.length).to.equal(5);
-
- for (const script of serialized.elements.scripts) {
- expect(script, script.name).to.have.property('script');
- expect(script.script, script.name).to.have.property('type').that.is.ok;
- }
+ describe('exports', () => {
+ it('has expected export', () => {
+ expect(flowExtensions).to.have.property('extensions').that.is.a('function');
+ expect(flowExtensions).to.have.property('extendFn').that.is.a('function');
+ expect(flowExtensions).to.have.property('OnifySequenceFlow').that.is.a('function');
+ expect(flowExtensions).to.have.property('OnifyTimerEventDefinition').that.is.a('function');
+ });
});
- it('extendFn registers extension scripts with type', async () => {
- const source = `
-
-
-
-
-
- next();
-
-
- next();
-
-
-
-
- `;
- const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
- const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn);
+ describe('extendFn', () => {
+ it('extendFn registers scripts', async () => {
+ const source = factory.resource('activedirectory-index-users.bpmn');
+ const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
+ const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn);
- expect(serialized.elements.scripts.length).to.equal(2);
+ expect(serialized.elements.scripts.length).to.equal(5);
- for (const script of serialized.elements.scripts) {
- expect(script, script.name).to.have.property('script');
- expect(script.script, script.name).to.have.property('type', 'camunda:ExecutionListener');
- }
- });
+ for (const script of serialized.elements.scripts) {
+ expect(script, script.name).to.have.property('script');
+ expect(script.script, script.name).to.have.property('type').that.is.ok;
+ }
+ });
- it('extendFn registers io scripts with type', async () => {
- const source = `
-
-
-
-
-
-
- next(null, 'GET');
-
- /my/items/workspace-1
-
- next(null, { id: content.id, statuscode });
-
-
-
-
-
- `;
- const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
- const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn);
+ it('extendFn registers extension scripts with type', async () => {
+ const source = `
+
+
+
+
+
+ next();
+
+
+ next();
+
+
+
+
+ `;
+ const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
+ const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn);
- const scripts = serialized.elements.scripts;
- expect(scripts.length).to.equal(2);
- expect(scripts[0], scripts[0].name).to.have.property('script');
- expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:InputOutput/camunda:InputParameter');
- expect(scripts[1], scripts[1].name).to.have.property('script');
- expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:InputOutput/camunda:OutputParameter');
- });
+ expect(serialized.elements.scripts.length).to.equal(2);
+
+ for (const script of serialized.elements.scripts) {
+ expect(script, script.name).to.have.property('script');
+ expect(script.script, script.name).to.have.property('type', 'camunda:ExecutionListener');
+ }
+ });
- it('extendFn registers connector io scripts with type', async () => {
- const source = `
-
-
-
-
-
- onifyApiRequest
+ it('extendFn registers io scripts with type', async () => {
+ const source = `
+
+
+
+
next(null, 'GET');
@@ -103,22 +79,57 @@ describe('extensions', () => {
next(null, { id: content.id, statuscode });
-
-
- \${content.output.result.statuscode}
-
-
-
-
- `;
- const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
- const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn);
+
+
+
+ `;
+ const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
+ const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn);
+
+ const scripts = serialized.elements.scripts;
+ expect(scripts.length).to.equal(2);
+ expect(scripts[0], scripts[0].name).to.have.property('script');
+ expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:InputOutput/camunda:InputParameter');
+ expect(scripts[1], scripts[1].name).to.have.property('script');
+ expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:InputOutput/camunda:OutputParameter');
+ });
+
+ it('extendFn registers connector io scripts with type', async () => {
+ const source = `
+
+
+
+
+
+ onifyApiRequest
+
+
+ next(null, 'GET');
+
+ /my/items/workspace-1
+
+ next(null, { id: content.id, statuscode });
+
+
+
+
+ \${content.output.result.statuscode}
+
+
+
+
+ `;
+ const moddleContext = await testHelpers.moddleContext(source, moddleExtensions);
+ const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn);
- const scripts = serialized.elements.scripts;
- expect(scripts.length).to.equal(2);
- expect(scripts[0], scripts[0].name).to.have.property('script');
- expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:Connector/camunda:InputParameter');
- expect(scripts[1], scripts[1].name).to.have.property('script');
- expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:Connector/camunda:OutputParameter');
+ const scripts = serialized.elements.scripts;
+ expect(scripts.length).to.equal(2);
+ expect(scripts[0], scripts[0].name).to.have.property('script');
+ expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:Connector/camunda:InputParameter');
+ expect(scripts[1], scripts[1].name).to.have.property('script');
+ expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:Connector/camunda:OutputParameter');
+ });
});
});
diff --git a/test/features/connector-feature.js b/test/features/connector-feature.js
index f48d6c0..f7f365b 100644
--- a/test/features/connector-feature.js
+++ b/test/features/connector-feature.js
@@ -1,4 +1,4 @@
-import ck from 'chronokinesis';
+import * as ck from 'chronokinesis';
import testHelpers from '../helpers/testHelpers.js';
import factory from '../helpers/factory.js';
diff --git a/test/features/engine-feature.js b/test/features/engine-feature.js
index 1350c29..4869d51 100644
--- a/test/features/engine-feature.js
+++ b/test/features/engine-feature.js
@@ -1,4 +1,4 @@
-import ck from 'chronokinesis';
+import * as ck from 'chronokinesis';
import factory from '../helpers/factory.js';
import testHelpers from '../helpers/testHelpers.js';
import {EventEmitter} from 'events';
diff --git a/test/features/execution-listener-feature.js b/test/features/execution-listener-feature.js
index 7390e5e..020555b 100644
--- a/test/features/execution-listener-feature.js
+++ b/test/features/execution-listener-feature.js
@@ -1,4 +1,4 @@
-import {default as Serializer, TypeResolver} from 'moddle-context-serializer';
+import {Serializer, TypeResolver} from 'moddle-context-serializer';
import {extendFn} from '../../src/index.js';
import * as Elements from 'bpmn-elements';
import factory from '../helpers/factory.js';
diff --git a/test/features/process-feature.js b/test/features/process-feature.js
index bc25b90..b13fd54 100644
--- a/test/features/process-feature.js
+++ b/test/features/process-feature.js
@@ -132,4 +132,193 @@ Feature('Flow process', () => {
expect(err.content.error).to.match(/Parser Error/i);
});
});
+
+ Scenario('Process with candidate starter groups and users in various formats', () => {
+ let source, flow;
+ Given('a flow with candidate groups as string split by comma with empty and blanks', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source);
+ });
+
+ let started;
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have groups in lowercase without empty', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admins', 'users']);
+ bp.stop();
+ });
+
+ Given('a flow with candidate groups expression referencing an environment array', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ settings: {
+ groups: ['admins', '', false, 'USERS'],
+ },
+ });
+ });
+
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have groups in without empty', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admins', 'USERS']);
+ bp.stop();
+ });
+
+ Given('a flow with candidate groups expression referencing an environment object', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ settings: {
+ groups: {foo: 'bar'},
+ },
+ });
+ });
+
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have NO groups since type is not accepted', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.not.have.property('candidateStarterGroups');
+ bp.stop();
+ });
+
+ Given('a flow ran with extension that sets candidate groups to string', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ extensions: {
+ myExtension(bp) {
+ if (bp.type !== 'bpmn:Process') return;
+ bp.behaviour.candidateStarterGroups = 'admin';
+ return {
+ activate() {},
+ deactivate() {},
+ };
+ },
+ },
+ });
+ });
+
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have groups', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admin']);
+ bp.stop();
+ });
+
+ Given('a flow ran with extension that sets candidate groups to array', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ extensions: {
+ myExtension(bp) {
+ if (bp.type !== 'bpmn:Process') return;
+ bp.behaviour.candidateStarterGroups = ['admin', '', 'Users'];
+ return {
+ activate() {},
+ deactivate() {},
+ };
+ },
+ },
+ });
+ });
+
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have groups', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admin', 'Users']);
+ bp.stop();
+ });
+
+ Given('a flow ran with extension that sets candidate groups to an object', async () => {
+ source = `
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ extensions: {
+ myExtension(bp) {
+ if (bp.type !== 'bpmn:Process') return;
+ bp.behaviour.candidateStarterGroups = {foo: 'bar'};
+ return {
+ activate() {},
+ deactivate() {},
+ };
+ },
+ },
+ });
+ });
+
+ When('started', () => {
+ started = flow.waitFor('process.start');
+ return flow.run();
+ });
+
+ Then('process have NO groups since type is not accepted', async () => {
+ const bp = await started;
+ expect(bp.environment.variables).to.not.have.property('candidateStarterGroups');
+ bp.stop();
+ });
+ });
});
diff --git a/test/features/recover-feature.js b/test/features/recover-feature.js
index c7c49af..187f6bf 100755
--- a/test/features/recover-feature.js
+++ b/test/features/recover-feature.js
@@ -1,4 +1,5 @@
import testHelpers from '../helpers/testHelpers.js';
+import factory from '../helpers/factory.js';
Feature('Recover flow', () => {
Scenario('A flow is recovered with output parameters', () => {
@@ -554,4 +555,66 @@ Feature('Recover flow', () => {
expect(flow.environment.output).to.have.property('result').that.deep.equal({ id: 'service', foo: 'bar', statuscode: 200 });
});
});
+
+ Scenario('recover different versions where elements has disappeared', () => {
+ let sourceV1, sourceV2, flow, options;
+ const serviceCalls = [];
+ Given('version 1 of a process with user tasks and a variety of elements', async () => {
+ sourceV1 = factory.resource('mother-of-all.bpmn');
+
+ options = {
+ services: {
+ onifyApiRequest(...args) {
+ serviceCalls.push(args);
+ args.pop()(null, { statuscode: 200 });
+ },
+ },
+ };
+
+ flow = await testHelpers.getOnifyFlow(sourceV1, options);
+ });
+
+ And('a second version where almost everything has disappeared', () => {
+ sourceV2 = `
+
+
+
+
+
+
+
+
+ `;
+ });
+
+ let execution, started;
+ When('started', async () => {
+ started = flow.waitFor('activity.start');
+ execution = await flow.run();
+ });
+
+ let state;
+ And('state is saved immediately after start', async () => {
+ await started;
+ await execution.stop();
+
+ state = await flow.getState();
+ });
+
+ let end;
+ When('definition recovered and resumed with second version', async () => {
+ flow = await testHelpers.recoverOnifyFlow(sourceV2, state, options);
+ end = flow.waitFor('end');
+ execution = await flow.resume();
+ });
+
+ And('pending task is signaled', () => {
+ const [pendingTask] = flow.getPostponed();
+ pendingTask.signal();
+ });
+
+ Then('flow run completes', () => {
+ return end;
+ });
+ });
});
diff --git a/test/features/serialize-feature.js b/test/features/serialize-feature.js
index e51e875..9b72c17 100644
--- a/test/features/serialize-feature.js
+++ b/test/features/serialize-feature.js
@@ -1,7 +1,7 @@
import testHelpers from '../helpers/testHelpers.js';
import factory from '../helpers/factory.js';
import * as Elements from 'bpmn-elements';
-import {default as Serializer, TypeResolver} from 'moddle-context-serializer';
+import {Serializer, TypeResolver} from 'moddle-context-serializer';
import {extendFn} from '../../src/index.js';
Feature('Extend function', () => {
@@ -54,5 +54,37 @@ Feature('Extend function', () => {
expect(registered.script).to.have.property('scriptFormat');
expect(registered.script).to.have.property('body');
});
+
+ Given('a source with sequence flow execution listener', async () => {
+ const source = factory.resource('sequence-flow-properties.bpmn');
+ moddleContext = await testHelpers.moddleContext(source, await testHelpers.getModdleExtensions());
+ });
+
+ When('serialized', () => {
+ serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn);
+ });
+
+ Then('all scripts are registered', () => {
+ expect(serialized.elements.scripts).to.have.length(4);
+ });
+
+ And('registered SequenceFlow execution listener scripts', () => {
+ const [listener0, listener1] = serialized.elements.scripts.filter((s) => s.parent.id === 'to-script');
+ expect(listener0).to.have.property('name', 'to-script/camunda:ExecutionListener/take/0');
+ expect(listener0).to.have.property('parent').that.deep.equal({
+ id: 'to-script',
+ type: 'bpmn:SequenceFlow',
+ });
+ expect(listener0.script).to.have.property('scriptFormat');
+ expect(listener0.script).to.have.property('body');
+
+ expect(listener1).to.have.property('name', 'to-script/camunda:ExecutionListener/take/1');
+ expect(listener1).to.have.property('parent').that.deep.equal({
+ id: 'to-script',
+ type: 'bpmn:SequenceFlow',
+ });
+ expect(listener1.script).to.have.property('scriptFormat');
+ expect(listener1.script).to.have.property('resource');
+ });
});
});
diff --git a/test/features/timers-feature.js b/test/features/timers-feature.js
index bdf823f..b5f5d91 100644
--- a/test/features/timers-feature.js
+++ b/test/features/timers-feature.js
@@ -1,6 +1,8 @@
-import ck from 'chronokinesis';
+import * as ck from 'chronokinesis';
+
import testHelpers from '../helpers/testHelpers.js';
import factory from '../helpers/factory.js';
+import {OnifyTimerEventDefinition} from '../../src/OnifyTimerEventDefinition.js';
Feature('Flow timers', () => {
let blueprintSource;
@@ -40,6 +42,38 @@ Feature('Flow timers', () => {
[element] = flow.getPostponed();
expect(element.type).to.equal('bpmn:ServiceTask');
});
+
+ describe('using OnifyTimerEventDefinition', () => {
+ When('started with extended TimerEventDefinition', async () => {
+ flow = await testHelpers.getOnifyFlow(blueprintSource, {
+ types: {
+ TimerEventDefinition: OnifyTimerEventDefinition,
+ },
+ });
+
+ ck.freeze(Date.UTC(2022, 1, 14, 12, 0));
+ flow.run();
+ });
+
+ Then('run is paused at start event', () => {
+ [element] = flow.getPostponed();
+ expect(element.type).to.equal('bpmn:StartEvent');
+ });
+
+ And('a timer is registered', () => {
+ [timer] = flow.environment.timers.executing;
+ expect(timer.delay).to.be.above(0).and.equal(Date.UTC(2022, 1, 15) - new Date().getTime());
+ });
+
+ When('cron start event is cancelled', () => {
+ flow.cancelActivity({id: element.id});
+ });
+
+ Then('flow continues run', () => {
+ [element] = flow.getPostponed();
+ expect(element.type).to.equal('bpmn:ServiceTask');
+ });
+ });
});
Scenario('Scheduled flow with date', () => {
diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js
index 9c09afa..7b538ac 100644
--- a/test/helpers/testHelpers.js
+++ b/test/helpers/testHelpers.js
@@ -1,4 +1,4 @@
-import {default as Serializer, TypeResolver} from 'moddle-context-serializer';
+import {Serializer, TypeResolver} from 'moddle-context-serializer';
import {Engine} from 'bpmn-engine';
import {extensions, extendFn} from '../../src/index.js';
import {FlowScripts} from './FlowScripts.js';
@@ -33,10 +33,7 @@ async function getOnifyFlow(source, options) {
}
const serialized = Serializer(moddle, TypeResolver({...Elements, ...options?.types}), extendFn);
- return new Elements.Definition(new Elements.Context(serialized), {
- ...getFlowOptions(serialized.name || serialized.id),
- ...options,
- });
+ return new Elements.Definition(new Elements.Context(serialized), getFlowOptions(serialized.name || serialized.id, options));
}
async function getEngine(name, source, options) {
@@ -44,9 +41,7 @@ async function getEngine(name, source, options) {
name,
source,
moddleOptions: await getModdleExtensions(),
- extensions: {onify: extensions},
- ...getFlowOptions(name),
- ...options,
+ ...getFlowOptions(name, options),
elements: {...Elements, ...options?.elements},
});
}
@@ -54,22 +49,20 @@ async function getEngine(name, source, options) {
async function recoverOnifyFlow(source, state, options) {
const moddle = await moddleContext(source, await getModdleExtensions());
const serialized = Serializer(moddle, TypeResolver(Elements), extendFn);
- return new Elements.Definition(new Elements.Context(serialized), {
- ...getFlowOptions(state.name || state.id),
- ...options,
- }).recover(state);
+ return new Elements.Definition(new Elements.Context(serialized), getFlowOptions(state.name || state.id, options)).recover(state);
}
-function getFlowOptions(name, options) {
+function getFlowOptions(name, options = {}) {
+ const {extensions: extensionsOption, services, ...rest} = options;
return {
Logger,
- extensions: {extensions},
+ extensions: {...extensionsOption, onify: extensions},
services: {
httpRequest() {},
onifyApiRequest() {},
onifyElevatedApiRequest() {},
parseJSON() {},
- ...options?.services,
+ ...services,
},
scripts: new FlowScripts(name, './test/resources', {
encrypt() {},
@@ -80,6 +73,7 @@ function getFlowOptions(name, options) {
},
}),
expressions,
+ ...rest,
};
}
diff --git a/test/resources/mother-of-all.bpmn b/test/resources/mother-of-all.bpmn
new file mode 100644
index 0000000..3c32a05
--- /dev/null
+++ b/test/resources/mother-of-all.bpmn
@@ -0,0 +1,384 @@
+
+
+
+
+
+
+
+
+
+ toFirstScriptTask
+
+
+ toFirstScriptTask
+ toReturnScriptTask
+ toFirstUserTask
+ next()
+
+
+ toFirstUserTask
+ toSubProcess
+
+
+ toSubProcess
+ toInclusiveGateway
+
+
+ toSubScriptTask
+
+
+ toSubScriptTaskTimeout
+
+ PT0.1S
+
+
+
+
+ toSubScriptTask
+ toSubScriptTaskTimeout
+ next();
+
+
+
+ toPickMe2
+ toJoin2
+
+
+ toJoin2
+ toJoin1
+ toJoin3
+ toDecision
+
+
+ toPickMe3
+ toJoin3
+
+
+ toPickMe1
+ toJoin1
+
+
+ toDecision
+ toLoop
+ toFinal
+
+
+ toLoop
+ toReturnScriptTask
+ environment.variables.stopLoop = true;
+next();
+
+
+ toInclusiveGateway
+ toPickMe2
+ toPickMe3
+ toPickMe1
+
+
+ toFinal
+
+
+
+
+ next(null, !environment.variables.stopLoop);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SequenceFlow_1ifeyo8
+
+
+
+ SequenceFlow_1uyrch1
+
+
+ SequenceFlow_0o4woz0
+
+
+
+ SequenceFlow_1ifeyo8
+ SequenceFlow_1uyrch1
+
+
+ SequenceFlow_0o4woz0
+
+
+
+
+
+
+ Use serviceFn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/resources/sequence-flow-properties.bpmn b/test/resources/sequence-flow-properties.bpmn
index 74db9c5..0608300 100644
--- a/test/resources/sequence-flow-properties.bpmn
+++ b/test/resources/sequence-flow-properties.bpmn
@@ -22,6 +22,9 @@ next();
baz
+
+
+
diff --git a/test/src/OnifyTimerEventDefinition-test.js b/test/src/OnifyTimerEventDefinition-test.js
new file mode 100644
index 0000000..f6d5093
--- /dev/null
+++ b/test/src/OnifyTimerEventDefinition-test.js
@@ -0,0 +1,112 @@
+import {Environment} from 'bpmn-elements';
+import * as ck from 'chronokinesis';
+import {Engine} from 'bpmn-engine';
+import * as bpmnElements from 'bpmn-elements';
+import * as bpmnElements8 from 'bpmn-elements-8-1';
+
+import {OnifyTimerEventDefinition} from '../../src/OnifyTimerEventDefinition.js';
+
+class TimerEventDefinition8Overload extends bpmnElements8.TimerEventDefinition {
+ parse() {
+ throw new Error('Not implemented');
+ }
+}
+
+describe('OnifyTimerEventDefinition', () => {
+ let def;
+ before(() => {
+ def = new OnifyTimerEventDefinition({
+ environment: new Environment(),
+ }, {
+ type: 'bpmn:TimerEventDefinition',
+ });
+ });
+ afterEach(ck.reset);
+
+ describe('engine', () => {
+ const source = `
+
+
+
+
+ \${environment.variables.cron}
+
+
+
+ `;
+
+ it('parses cron when using extended TimerEventDefinition', async () => {
+ const engine = new Engine({
+ source,
+ elements: {
+ ...bpmnElements,
+ TimerEventDefinition: OnifyTimerEventDefinition,
+ },
+ variables: {
+ cron: '* * * * *',
+ },
+ });
+
+ ck.freeze(Date.UTC(2023, 4, 27));
+ const execution = await engine.execute();
+
+ const [activityApi] = execution.getPostponed();
+ expect(activityApi.getExecuting()[0].content).to.have.property('timeout', 60000);
+
+ engine.stop();
+ });
+
+ it('ignores cron parse if bpmn-elements < 10 is used', async () => {
+ const engine = new Engine({
+ source,
+ elements: {
+ ...bpmnElements8,
+ TimerEventDefinition: TimerEventDefinition8Overload,
+ },
+ variables: {
+ cron: '* * * * *',
+ },
+ });
+
+ ck.freeze(Date.UTC(2023, 4, 27));
+ const execution = await engine.execute();
+
+ const [activityApi] = execution.getPostponed();
+ expect(activityApi.getExecuting()[0].content).to.not.have.property('timeout');
+
+ engine.stop();
+ });
+ });
+
+ describe('parse overload', () => {
+ it('parses standard cron time cycle', () => {
+ ck.freeze(Date.UTC(2023, 4, 27));
+ expect(def.parse('timeCycle', '* * * * *')).to.deep.equal({delay: 60000, expireAt: new Date('2023-05-27T00:01:00.000Z')});
+ });
+
+ it('parses fractional cron time cycle', () => {
+ ck.freeze(Date.UTC(2023, 4, 27));
+ expect(def.parse('timeCycle', '*/5 * * * * *')).to.deep.equal({delay: 5000, expireAt: new Date('2023-05-27T00:00:05.000Z')});
+ });
+
+ it('parses every 5 minutes cron time cycle', () => {
+ ck.freeze(Date.UTC(2023, 4, 27));
+ expect(def.parse('timeCycle', '*/5 * * * *')).to.deep.equal({delay: 300000, expireAt: new Date('2023-05-27T00:05:00.000Z')});
+ });
+
+ it('returns ISO duration if no cron match', () => {
+ ck.freeze(Date.UTC(2023, 4, 27));
+ expect(def.parse('timeCycle', 'PT10S')).to.deep.contain({delay: 10000, expireAt: new Date('2023-05-27T00:00:10.000Z')});
+ });
+
+ it('throws if invalid time cycle', () => {
+ ck.freeze(Date.UTC(2023, 4, 27));
+ expect(() => {
+ def.parse('timeCycle', 'yesterday');
+ }).to.throw('invalid');
+ });
+ });
+});