Skip to content

Commit bb992e3

Browse files
committed
feat: simplify and unify execution callbacks
- Unify callbacks into onTaskExecutionFinished with reason field - Remove onTaskExecutionInterrupted Result object now uses discriminated union: - success: true → contains variables - success: false → contains reason (incident/user.cancel/user.selectionChanged/error) BREAKING CHANGE: onTaskExecutionInterrupted removed. Use onTaskExecutionFinished with result.success and result.reason instead. Related to camunda/camunda-modeler#5597
1 parent 34f6769 commit bb992e3

File tree

7 files changed

+515
-50
lines changed

7 files changed

+515
-50
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,41 @@ import TaskTesting from '@camunda/task-testing';
1212
function App() {
1313
...
1414

15-
<TaskTesting api={ ... }>
15+
<TaskTesting
16+
api={ ... }
17+
onTaskExecutionStarted={ (element) => { ... } }
18+
onTaskExecutionFinished={ (element, result) => { ... } }
19+
onTaskExecutionError={ (element, error) => { ... } }
20+
>
1621
<TaskTesting.Tab label={ 'Foo' }>...</TaskTesting.Tab>;
1722
<TaskTesting.Link href="https://camunda.com">Foo</TaskTesting.Link>;
1823
</TaskTesting>
1924
}
2025
```
2126

27+
### Props
28+
29+
#### Lifecycle Callbacks
30+
31+
The TaskTesting component provides callbacks to track task execution lifecycle events:
32+
33+
- **`onTaskExecutionStarted(element)`** - Called when a task execution begins
34+
- `element` - The BPMN element being tested
35+
36+
- **`onTaskExecutionFinished(element, result)`** - Called when task execution ends (success, incident, or cancellation)
37+
- `element` - The BPMN element that was tested
38+
- `result` - Execution result object:
39+
- When `success: true`: Contains `variables` with execution output
40+
- When `success: false`: Contains `reason` explaining why:
41+
- `'incident'` - Task completed with an incident (includes `incident` and optional `variables`)
42+
- `'user.cancel'` - User clicked cancel button
43+
- `'user.selectionChanged'` - User selected a different element
44+
- `'error'` - Deployment or start instance failed (includes `error` object)
45+
46+
- **`onTaskExecutionError(element, error)`** - Called when deployment or instance start fails (fired before `onTaskExecutionFinished`)
47+
- `element` - The BPMN element being tested
48+
- `error` - Error object with `message` and `response` details
49+
2250
[See demo](https://github.com/camunda/task-testing/tree/main/demo)
2351

2452
## Development

demo/App.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ function App() {
162162
onConfigChanged={ onConfigChanged }
163163
operateBaseUrl={ operateURL }
164164
documentationUrl="https://docs.camunda.io/"
165+
onTaskExecutionStarted={ (element) => {
166+
console.log('Task execution started:', element.id);
167+
} }
168+
onTaskExecutionFinished={ (element, result) => {
169+
console.log('Task execution finished:', element.id, result.success ? 'success' : result.reason, result);
170+
} }
165171
/>
166172
</div>
167173
</>

lib/TaskExecution.js

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ export const SCOPES = {
3838
/**
3939
* Emits:
4040
* - `taskExecution.status.changed` with one of {@link TaskExecutionStatus}
41-
* - `taskExecution.finished` with {@link TaskExecutionResult}
41+
* - `taskExecution.finished` with {@link TaskExecutionResult} (includes all completion outcomes: success, incident, cancellations)
4242
* - `taskExecution.error` with {@link TaskExecutionError}
43-
* - `taskExecution.interrupted` when execution is interrupted by switching focus
4443
*/
4544
export default class TaskExecution extends EventEmitter {
4645

@@ -62,10 +61,7 @@ export default class TaskExecution extends EventEmitter {
6261
const eventBus = injector.get('eventBus');
6362

6463
eventBus.on('selection.changed', () => {
65-
if (this._status !== 'idle') {
66-
this.emit('taskExecution.interrupted');
67-
}
68-
this.cancelTaskExecution();
64+
this.cancelTaskExecution('user.selectionChanged');
6965
});
7066
}
7167

@@ -96,17 +92,19 @@ export default class TaskExecution extends EventEmitter {
9692
}
9793

9894
if (!deploymentResult.success) {
99-
this._emitError('Failed to deploy process definition', deploymentResult.error);
100-
this.cancelTaskExecution();
95+
const error = { message: 'Failed to deploy process definition', response: deploymentResult.error };
96+
this._emitError(error.message, error.response);
97+
this.cancelTaskExecution('error', error);
10198
return;
10299
}
103100

104101
const processDefinitionKey = getProcessDefinitionKey(deploymentResult.response, processId);
105102

106103
if (!processDefinitionKey) {
107-
this._emitError('Failed to retrieve process definition key from deployment response', deploymentResult.response);
104+
const error = { message: 'Failed to retrieve process definition key from deployment response', response: deploymentResult.response };
105+
this._emitError(error.message, error.response);
108106

109-
this.cancelTaskExecution();
107+
this.cancelTaskExecution('error', error);
110108

111109
return;
112110
}
@@ -122,16 +120,18 @@ export default class TaskExecution extends EventEmitter {
122120
}
123121

124122
if (!startInstanceResult.success) {
125-
this._emitError('Failed to start process instance', startInstanceResult.error);
126-
this.cancelTaskExecution();
123+
const error = { message: 'Failed to start process instance', response: startInstanceResult.error };
124+
this._emitError(error.message, error.response);
125+
this.cancelTaskExecution('error', error);
127126
return;
128127
}
129128

130129
const processInstanceKey = getProcessInstanceKey(startInstanceResult.response);
131130

132131
if (!processInstanceKey) {
133-
this._emitError('Failed to retrieve process instance key from start instance response');
134-
this.cancelTaskExecution();
132+
const error = { message: 'Failed to retrieve process instance key from start instance response' };
133+
this._emitError(error.message);
134+
this.cancelTaskExecution('error', error);
135135
return;
136136
}
137137

@@ -220,10 +220,14 @@ export default class TaskExecution extends EventEmitter {
220220
element.id
221221
);
222222

223-
this.emit('taskExecution.finished', {
224-
success: !incident,
223+
this.emit('taskExecution.finished', incident ? {
224+
success: false,
225+
reason: 'incident',
225226
incident,
226227
variables
228+
} : {
229+
success: true,
230+
variables
227231
});
228232

229233
this.cancelTaskExecution();
@@ -235,19 +239,35 @@ export default class TaskExecution extends EventEmitter {
235239

236240
/**
237241
* Cancel current task execution, clean up and change status to `idle`.
242+
*
243+
* @param {string} [reason] - Reason for cancellation: 'user.cancel', 'user.selectionChanged', or 'error'
244+
* @param {any} [error] - Error object when reason is 'error'
238245
*/
239-
async cancelTaskExecution() {
246+
async cancelTaskExecution(reason, error) {
240247

241248
// TODO: Proper clean up:
242249
// - delete process instance
243250
// - delete process definition
244251
// - cancel deploy and start instance if they are in progress
245252

253+
const wasCanceled = this._status !== 'idle';
254+
246255
if (this._interval) {
247256
clearInterval(this._interval);
248257
}
249258

250259
this._changeStatus('idle');
260+
261+
if (wasCanceled && reason) {
262+
this.emit('taskExecution.finished', reason === 'error' ? {
263+
success: false,
264+
reason,
265+
error
266+
} : {
267+
success: false,
268+
reason
269+
});
270+
}
251271
}
252272

253273
/**

lib/components/TaskTesting/TaskTesting.js

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ import { OutputTab } from '../Output/OutputVariables';
5252
* @param {string} [props.operateBaseUrl]
5353
* @param {string} [props.documentationUrl]
5454
* @param {Function} [props.onTaskExecutionStarted=() => {}]
55-
* @param {Function} [props.onTaskExecutionFinished=() => {}]
56-
* @param {Function} [props.onTaskExecutionInterrupted=() => {}]
55+
* @param {Function} [props.onTaskExecutionFinished=() => {}] - Called with (element, result) where result contains success and optional reason for failures
5756
* @param {React.ReactNode[]} [props.children=[]]
5857
*/
5958
export default function TaskTesting({
@@ -71,7 +70,6 @@ export default function TaskTesting({
7170
documentationUrl,
7271
onTaskExecutionStarted = () => {},
7372
onTaskExecutionFinished = () => {},
74-
onTaskExecutionInterrupted = () => {},
7573
children = []
7674
}) {
7775

@@ -229,30 +227,30 @@ export default function TaskTesting({
229227
}
230228
};
231229

232-
/** @param {import('../../types').ElementOutput} output */
233-
const handleFinished = (output) => {
234-
elementConfigRef?.current?.setOutputConfigForElement(element, {
235-
...output,
236-
operateUrl: currentOperateUrl
237-
});
238-
onTaskExecutionFinished(element, output);
239-
};
240-
241-
const handleInterrupted = () => {
242-
onTaskExecutionInterrupted();
230+
/** @param {import('../../types').TaskExecutionResult} result */
231+
const handleFinished = (result) => {
232+
233+
// Only save to config if it's a successful execution or incident (not cancellations)
234+
if (result.success || result.reason === 'incident') {
235+
elementConfigRef?.current?.setOutputConfigForElement(element, {
236+
success: result.success,
237+
variables: result.variables,
238+
incident: result.success ? undefined : result.incident,
239+
operateUrl: currentOperateUrl
240+
});
241+
}
242+
onTaskExecutionFinished(element, result);
243243
};
244244

245245
taskExecutionRef?.current?.on('taskExecution.finished', handleFinished);
246246
taskExecutionRef?.current?.on('taskExecution.status.changed', handleStatusChange);
247247
taskExecutionRef?.current?.on('taskExecution.error', handleError);
248-
taskExecutionRef?.current?.on('taskExecution.interrupted', handleInterrupted);
249248

250249
return () => {
251250
if (taskExecutionRef.current) {
252251
taskExecutionRef.current.off('taskExecution.finished', handleFinished);
253252
taskExecutionRef.current.off('taskExecution.status.changed', handleStatusChange);
254253
taskExecutionRef.current.off('taskExecution.error', handleError);
255-
taskExecutionRef.current.off('taskExecution.interrupted', handleInterrupted);
256254
}
257255
};
258256
}, [ element, operateBaseUrl, currentOperateUrl ]);
@@ -308,7 +306,7 @@ export default function TaskTesting({
308306
};
309307

310308
const handleCancelTaskExecution = () => {
311-
taskExecutionRef?.current?.cancelTaskExecution();
309+
taskExecutionRef?.current?.cancelTaskExecution('user.cancel');
312310
};
313311

314312
const handleResetOutput = useCallback(() => {

lib/types.d.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,34 @@ export type TaskExecutionStatus =
7878
export type TaskExecutionEvents =
7979
'taskExecution.status.changed' |
8080
'taskExecution.finished' |
81-
'taskExecution.error' |
82-
'taskExecution.interrupted';
81+
'taskExecution.error';
8382

84-
export type TaskExecutionResult = {
85-
success: boolean;
86-
variables?: ElementOutputVariables;
87-
error?: TaskExecutionError;
88-
incident?: any;
89-
}
83+
export type TaskExecutionResult =
84+
// Success case - no reason needed
85+
| {
86+
success: true;
87+
variables: ElementOutputVariables;
88+
}
89+
// Failure cases - always have reason
90+
| {
91+
success: false;
92+
reason: 'incident';
93+
incident: any;
94+
variables?: ElementOutputVariables;
95+
}
96+
| {
97+
success: false;
98+
reason: 'user.cancel';
99+
}
100+
| {
101+
success: false;
102+
reason: 'user.selectionChanged';
103+
}
104+
| {
105+
success: false;
106+
reason: 'error';
107+
error: TaskExecutionError;
108+
};
90109

91110
export type TaskExecutionError = {
92111
message: string;

0 commit comments

Comments
 (0)