@bpmnkit/plugins 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/ai-bridge/css.js +77 -0
- package/dist/ai-bridge/panel.js +29 -3
- package/dist/deploy/css.d.ts +4 -0
- package/dist/deploy/css.js +150 -0
- package/dist/deploy/index.d.ts +25 -0
- package/dist/deploy/index.js +412 -0
- package/dist/element-docs/index.js +54 -0
- package/dist/live-mode/css.d.ts +4 -0
- package/dist/live-mode/css.js +170 -0
- package/dist/live-mode/index.d.ts +42 -0
- package/dist/live-mode/index.js +479 -0
- package/dist/pattern-advisor/css.d.ts +4 -0
- package/dist/pattern-advisor/css.js +198 -0
- package/dist/pattern-advisor/index.d.ts +20 -0
- package/dist/pattern-advisor/index.js +233 -0
- package/dist/process-runner/css.js +128 -0
- package/dist/process-runner/index.d.ts +52 -0
- package/dist/process-runner/index.js +508 -19
- package/dist/storage/storage-api.d.ts +6 -0
- package/dist/storage/storage-api.js +12 -0
- package/dist/story-view/css.d.ts +4 -0
- package/dist/story-view/css.js +259 -0
- package/dist/story-view/index.d.ts +33 -0
- package/dist/story-view/index.js +294 -0
- package/dist/variable-flow/css.d.ts +2 -0
- package/dist/variable-flow/css.js +98 -0
- package/dist/variable-flow/index.d.ts +12 -0
- package/dist/variable-flow/index.js +226 -0
- package/package.json +26 -6
|
@@ -2,9 +2,16 @@ import { injectProcessRunnerStyles } from "./css.js";
|
|
|
2
2
|
// ── IndexedDB persistence for input variables ───────────────────────────────
|
|
3
3
|
function openRunnerDb() {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
|
-
const req = indexedDB.open("bpmnkit-process-runner-v1",
|
|
6
|
-
req.onupgradeneeded = () => {
|
|
7
|
-
req.result
|
|
5
|
+
const req = indexedDB.open("bpmnkit-process-runner-v1", 2);
|
|
6
|
+
req.onupgradeneeded = (e) => {
|
|
7
|
+
const db = req.result;
|
|
8
|
+
if (!db.objectStoreNames.contains("data")) {
|
|
9
|
+
db.createObjectStore("data");
|
|
10
|
+
}
|
|
11
|
+
// Version 2: add scenarios store keyed by projectId
|
|
12
|
+
if (e.oldVersion < 2 && !db.objectStoreNames.contains("scenarios")) {
|
|
13
|
+
db.createObjectStore("scenarios");
|
|
14
|
+
}
|
|
8
15
|
};
|
|
9
16
|
req.onsuccess = () => resolve(req.result);
|
|
10
17
|
req.onerror = () => reject(req.error);
|
|
@@ -46,8 +53,62 @@ async function saveInputVars(projectId, vars) {
|
|
|
46
53
|
// ignore — IndexedDB unavailable
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
function scenariosKey(projectId) {
|
|
57
|
+
return projectId !== null ? `scenarios:${projectId}` : "scenarios";
|
|
58
|
+
}
|
|
59
|
+
async function loadScenarios(projectId) {
|
|
60
|
+
try {
|
|
61
|
+
const db = await openRunnerDb();
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const req = db
|
|
64
|
+
.transaction("scenarios", "readonly")
|
|
65
|
+
.objectStore("scenarios")
|
|
66
|
+
.get(scenariosKey(projectId));
|
|
67
|
+
req.onsuccess = () => {
|
|
68
|
+
const raw = req.result;
|
|
69
|
+
resolve(Array.isArray(raw) ? raw : []);
|
|
70
|
+
};
|
|
71
|
+
req.onerror = () => resolve([]);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function saveScenarios(projectId, scenarios) {
|
|
79
|
+
try {
|
|
80
|
+
const db = await openRunnerDb();
|
|
81
|
+
await new Promise((resolve) => {
|
|
82
|
+
const tx = db.transaction("scenarios", "readwrite");
|
|
83
|
+
tx.objectStore("scenarios").put(scenarios, scenariosKey(projectId));
|
|
84
|
+
tx.oncomplete = () => resolve();
|
|
85
|
+
tx.onerror = () => resolve();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
49
92
|
// ── Internal state ──────────────────────────────────────────────────────────
|
|
50
93
|
const AUTO_PLAY_DELAY_MS = 600;
|
|
94
|
+
/** Default probability (0–1) that a service task is chaos-injected. */
|
|
95
|
+
const CHAOS_FAILURE_PROBABILITY = 0.2;
|
|
96
|
+
// ── Chaos helpers ───────────────────────────────────────────────────────────
|
|
97
|
+
/** Element types that are eligible for chaos injection (worker tasks). */
|
|
98
|
+
const CHAOS_ELIGIBLE_TYPES = new Set(["serviceTask", "sendTask", "businessRuleTask", "scriptTask"]);
|
|
99
|
+
function buildChaosSchedule(elementIds, probability) {
|
|
100
|
+
const schedule = new Map();
|
|
101
|
+
const types = ["service-failure", "null-response", "random-delay"];
|
|
102
|
+
for (const id of elementIds) {
|
|
103
|
+
if (Math.random() < probability) {
|
|
104
|
+
const injType = types[Math.floor(Math.random() * types.length)];
|
|
105
|
+
if (injType !== undefined) {
|
|
106
|
+
schedule.set(id, { elementId: id, type: injType });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return schedule;
|
|
111
|
+
}
|
|
51
112
|
// ── Plugin factory ──────────────────────────────────────────────────────────
|
|
52
113
|
const PLAY_ICON = '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4 2.5l10 5.5-10 5.5V2.5z"/></svg>';
|
|
53
114
|
export function createProcessRunnerPlugin(options) {
|
|
@@ -69,6 +130,20 @@ export function createProcessRunnerPlugin(options) {
|
|
|
69
130
|
const variables = new Map();
|
|
70
131
|
/** Errors emitted during the current run. */
|
|
71
132
|
const errors = [];
|
|
133
|
+
// ── Time-travel debugger state ──────────────────────────────────────────
|
|
134
|
+
const MAX_EVENT_LOG = 10_000;
|
|
135
|
+
/** Full ordered event log for the current run. */
|
|
136
|
+
const eventLog = [];
|
|
137
|
+
/** null = live (tail of log); number = scrubbed to that index. */
|
|
138
|
+
let scrubIndex = null;
|
|
139
|
+
/** Whether chaos mode is enabled. */
|
|
140
|
+
let chaosEnabled = false;
|
|
141
|
+
/** Active chaos schedule for the current run. */
|
|
142
|
+
let chaosSchedule = new Map();
|
|
143
|
+
/** Injections that triggered during the last chaos run (cleared on new run). */
|
|
144
|
+
const lastChaosInjections = [];
|
|
145
|
+
/** True = last chaos run completed; false = failed/stuck; null = no chaos run yet. */
|
|
146
|
+
let lastChaosRunCompleted = null;
|
|
72
147
|
/** Input variables configured by the user (persisted in IndexedDB). */
|
|
73
148
|
const inputVars = [];
|
|
74
149
|
const toolbarEl = document.createElement("div");
|
|
@@ -95,10 +170,12 @@ export function createProcessRunnerPlugin(options) {
|
|
|
95
170
|
const feelTabBtn = makeTabBtn("FEEL", false);
|
|
96
171
|
const errorsTabBtn = makeTabBtn("Errors", false);
|
|
97
172
|
const inputTabBtn = makeTabBtn("Input", false);
|
|
173
|
+
const testsTabBtn = makeTabBtn("Tests", false);
|
|
98
174
|
playTabBarEl.appendChild(varTabBtn);
|
|
99
175
|
playTabBarEl.appendChild(feelTabBtn);
|
|
100
176
|
playTabBarEl.appendChild(errorsTabBtn);
|
|
101
177
|
playTabBarEl.appendChild(inputTabBtn);
|
|
178
|
+
playTabBarEl.appendChild(testsTabBtn);
|
|
102
179
|
function makePaneEl(hidden) {
|
|
103
180
|
const d = document.createElement("div");
|
|
104
181
|
d.className = hidden
|
|
@@ -110,25 +187,138 @@ export function createProcessRunnerPlugin(options) {
|
|
|
110
187
|
const feelPaneEl = makePaneEl(true);
|
|
111
188
|
const errorsPaneEl = makePaneEl(true);
|
|
112
189
|
const ivarsPaneEl = makePaneEl(true);
|
|
190
|
+
const testsPaneEl = makePaneEl(true);
|
|
191
|
+
// ── Timeline scrubber ───────────────────────────────────────────────────
|
|
192
|
+
const scrubberRowEl = document.createElement("div");
|
|
193
|
+
scrubberRowEl.className = "bpmnkit-runner-scrubber-row";
|
|
194
|
+
scrubberRowEl.style.display = "none";
|
|
195
|
+
const scrubberEl = document.createElement("input");
|
|
196
|
+
scrubberEl.type = "range";
|
|
197
|
+
scrubberEl.className = "bpmnkit-runner-scrubber";
|
|
198
|
+
scrubberEl.min = "0";
|
|
199
|
+
scrubberEl.max = "0";
|
|
200
|
+
scrubberEl.value = "0";
|
|
201
|
+
const scrubberLiveBtn = document.createElement("button");
|
|
202
|
+
scrubberLiveBtn.className = "bpmnkit-runner-scrubber-live";
|
|
203
|
+
scrubberLiveBtn.textContent = "Live";
|
|
204
|
+
const scrubberReplayBtn = document.createElement("button");
|
|
205
|
+
scrubberReplayBtn.className = "bpmnkit-runner-scrubber-replay";
|
|
206
|
+
scrubberReplayBtn.textContent = "Replay from here";
|
|
207
|
+
scrubberReplayBtn.style.display = "none";
|
|
208
|
+
const scrubberIndexEl = document.createElement("span");
|
|
209
|
+
scrubberIndexEl.className = "bpmnkit-runner-scrubber-index";
|
|
210
|
+
scrubberRowEl.appendChild(scrubberEl);
|
|
211
|
+
scrubberRowEl.appendChild(scrubberIndexEl);
|
|
212
|
+
scrubberRowEl.appendChild(scrubberLiveBtn);
|
|
213
|
+
scrubberRowEl.appendChild(scrubberReplayBtn);
|
|
214
|
+
playPanelEl.appendChild(scrubberRowEl);
|
|
113
215
|
playPanelEl.appendChild(playTabBarEl);
|
|
114
216
|
playPanelEl.appendChild(varsPaneEl);
|
|
115
217
|
playPanelEl.appendChild(feelPaneEl);
|
|
116
218
|
playPanelEl.appendChild(errorsPaneEl);
|
|
117
219
|
playPanelEl.appendChild(ivarsPaneEl);
|
|
220
|
+
playPanelEl.appendChild(testsPaneEl);
|
|
118
221
|
function switchPlayTab(tab) {
|
|
119
222
|
varTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "variables");
|
|
120
223
|
feelTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "feel");
|
|
121
224
|
errorsTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "errors");
|
|
122
225
|
inputTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "input");
|
|
226
|
+
testsTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "tests");
|
|
123
227
|
varsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "variables");
|
|
124
228
|
feelPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "feel");
|
|
125
229
|
errorsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "errors");
|
|
126
230
|
ivarsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "input");
|
|
231
|
+
testsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "tests");
|
|
127
232
|
}
|
|
128
233
|
varTabBtn.addEventListener("click", () => switchPlayTab("variables"));
|
|
129
234
|
feelTabBtn.addEventListener("click", () => switchPlayTab("feel"));
|
|
130
235
|
errorsTabBtn.addEventListener("click", () => switchPlayTab("errors"));
|
|
131
236
|
inputTabBtn.addEventListener("click", () => switchPlayTab("input"));
|
|
237
|
+
testsTabBtn.addEventListener("click", () => switchPlayTab("tests"));
|
|
238
|
+
function computeStateAt(idx) {
|
|
239
|
+
const vars = new Map();
|
|
240
|
+
const feels = [];
|
|
241
|
+
const errs = [];
|
|
242
|
+
const capped = Math.min(idx, eventLog.length - 1);
|
|
243
|
+
for (let i = 0; i <= capped; i++) {
|
|
244
|
+
const evt = eventLog[i];
|
|
245
|
+
if (evt === undefined)
|
|
246
|
+
continue;
|
|
247
|
+
if (evt.type === "variable:set" && typeof evt.name === "string") {
|
|
248
|
+
vars.set(evt.name, evt.value);
|
|
249
|
+
}
|
|
250
|
+
else if (evt.type === "feel:evaluated" &&
|
|
251
|
+
typeof evt.elementId === "string" &&
|
|
252
|
+
typeof evt.property === "string" &&
|
|
253
|
+
typeof evt.expression === "string") {
|
|
254
|
+
feels.push({
|
|
255
|
+
elementId: evt.elementId,
|
|
256
|
+
property: evt.property,
|
|
257
|
+
expression: evt.expression,
|
|
258
|
+
result: evt.result,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else if (evt.type === "element:failed") {
|
|
262
|
+
if (typeof evt.elementId === "string" && typeof evt.error === "string") {
|
|
263
|
+
errs.push({ elementId: evt.elementId, message: evt.error });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (evt.type === "process:failed" && typeof evt.error === "string") {
|
|
267
|
+
errs.push({ message: evt.error });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { variables: vars, feelEvals: feels, errors: errs };
|
|
271
|
+
}
|
|
272
|
+
function updateScrubber() {
|
|
273
|
+
const len = eventLog.length;
|
|
274
|
+
if (len === 0) {
|
|
275
|
+
scrubberRowEl.style.display = "none";
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
scrubberRowEl.style.display = "";
|
|
279
|
+
scrubberEl.max = String(len - 1);
|
|
280
|
+
const isLive = scrubIndex === null;
|
|
281
|
+
scrubberEl.value = isLive ? String(len - 1) : String(scrubIndex);
|
|
282
|
+
scrubberIndexEl.textContent = isLive
|
|
283
|
+
? `${len} events (live)`
|
|
284
|
+
: `Event ${(scrubIndex ?? 0) + 1} / ${len}`;
|
|
285
|
+
scrubberLiveBtn.style.display = isLive ? "none" : "";
|
|
286
|
+
scrubberReplayBtn.style.display = isLive ? "none" : "";
|
|
287
|
+
}
|
|
288
|
+
function renderAtCurrentScrub() {
|
|
289
|
+
if (scrubIndex === null) {
|
|
290
|
+
renderVariables();
|
|
291
|
+
renderFeelEvals();
|
|
292
|
+
renderErrors();
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const state = computeStateAt(scrubIndex);
|
|
296
|
+
renderVariables(state.variables);
|
|
297
|
+
renderFeelEvals(state.feelEvals);
|
|
298
|
+
renderErrors(state.errors);
|
|
299
|
+
}
|
|
300
|
+
updateScrubber();
|
|
301
|
+
}
|
|
302
|
+
scrubberEl.addEventListener("input", () => {
|
|
303
|
+
const idx = Number(scrubberEl.value);
|
|
304
|
+
scrubIndex = idx >= eventLog.length - 1 ? null : idx;
|
|
305
|
+
renderAtCurrentScrub();
|
|
306
|
+
});
|
|
307
|
+
scrubberLiveBtn.addEventListener("click", () => {
|
|
308
|
+
scrubIndex = null;
|
|
309
|
+
renderAtCurrentScrub();
|
|
310
|
+
});
|
|
311
|
+
scrubberReplayBtn.addEventListener("click", () => {
|
|
312
|
+
if (scrubIndex === null)
|
|
313
|
+
return;
|
|
314
|
+
// Snapshot variables at the scrub point and re-run
|
|
315
|
+
const state = computeStateAt(scrubIndex);
|
|
316
|
+
const initVars = {};
|
|
317
|
+
for (const [k, v] of state.variables)
|
|
318
|
+
initVars[k] = v;
|
|
319
|
+
scrubIndex = null;
|
|
320
|
+
startInstance(initVars);
|
|
321
|
+
});
|
|
132
322
|
// ── Render functions ────────────────────────────────────────────────────
|
|
133
323
|
function emptyEl(text) {
|
|
134
324
|
const d = document.createElement("div");
|
|
@@ -140,13 +330,14 @@ export function createProcessRunnerPlugin(options) {
|
|
|
140
330
|
while (el.firstChild !== null)
|
|
141
331
|
el.removeChild(el.firstChild);
|
|
142
332
|
}
|
|
143
|
-
function renderVariables() {
|
|
333
|
+
function renderVariables(snapshot) {
|
|
144
334
|
clearEl(varsPaneEl);
|
|
145
|
-
|
|
335
|
+
const src = snapshot ?? variables;
|
|
336
|
+
if (src.size === 0) {
|
|
146
337
|
varsPaneEl.appendChild(emptyEl("No variables yet."));
|
|
147
338
|
return;
|
|
148
339
|
}
|
|
149
|
-
for (const [name, value] of
|
|
340
|
+
for (const [name, value] of src) {
|
|
150
341
|
const row = document.createElement("div");
|
|
151
342
|
row.className = "bpmnkit-runner-play-var-row";
|
|
152
343
|
const nameEl = document.createElement("span");
|
|
@@ -160,14 +351,15 @@ export function createProcessRunnerPlugin(options) {
|
|
|
160
351
|
varsPaneEl.appendChild(row);
|
|
161
352
|
}
|
|
162
353
|
}
|
|
163
|
-
function renderFeelEvals() {
|
|
354
|
+
function renderFeelEvals(snapshot) {
|
|
164
355
|
clearEl(feelPaneEl);
|
|
165
|
-
|
|
356
|
+
const src = snapshot ?? feelEvals;
|
|
357
|
+
if (src.length === 0) {
|
|
166
358
|
feelPaneEl.appendChild(emptyEl("No FEEL expressions evaluated yet."));
|
|
167
359
|
return;
|
|
168
360
|
}
|
|
169
361
|
const groups = new Map();
|
|
170
|
-
for (const ev of
|
|
362
|
+
for (const ev of src) {
|
|
171
363
|
let arr = groups.get(ev.elementId);
|
|
172
364
|
if (arr === undefined) {
|
|
173
365
|
arr = [];
|
|
@@ -209,13 +401,23 @@ export function createProcessRunnerPlugin(options) {
|
|
|
209
401
|
feelPaneEl.appendChild(groupEl);
|
|
210
402
|
}
|
|
211
403
|
}
|
|
212
|
-
function renderErrors() {
|
|
404
|
+
function renderErrors(snapshot) {
|
|
213
405
|
clearEl(errorsPaneEl);
|
|
214
|
-
|
|
406
|
+
// Chaos summary banner — only in live (non-scrubbed) mode after a chaos run
|
|
407
|
+
if (snapshot === undefined && chaosEnabled && lastChaosRunCompleted !== null) {
|
|
408
|
+
const banner = document.createElement("div");
|
|
409
|
+
banner.className = "bpmnkit-runner-chaos-summary";
|
|
410
|
+
const stuck = lastChaosRunCompleted ? 0 : 1;
|
|
411
|
+
const errCount = errors.filter((e) => e.elementId !== undefined).length;
|
|
412
|
+
banner.textContent = `Chaos run: ${stuck > 0 ? "1 stuck instance" : "completed"}, ${errCount} unhandled error${errCount !== 1 ? "s" : ""} found`;
|
|
413
|
+
errorsPaneEl.appendChild(banner);
|
|
414
|
+
}
|
|
415
|
+
const src = snapshot ?? errors;
|
|
416
|
+
if (src.length === 0) {
|
|
215
417
|
errorsPaneEl.appendChild(emptyEl("No errors."));
|
|
216
418
|
return;
|
|
217
419
|
}
|
|
218
|
-
for (const err of
|
|
420
|
+
for (const err of src) {
|
|
219
421
|
const rowEl = document.createElement("div");
|
|
220
422
|
rowEl.className = "bpmnkit-runner-play-error-row";
|
|
221
423
|
if (err.elementId !== undefined) {
|
|
@@ -306,6 +508,169 @@ export function createProcessRunnerPlugin(options) {
|
|
|
306
508
|
});
|
|
307
509
|
ivarsPaneEl.appendChild(addBtn);
|
|
308
510
|
}
|
|
511
|
+
// ── Tests tab ────────────────────────────────────────────────────────────
|
|
512
|
+
const scenarios = [];
|
|
513
|
+
const scenarioResults = new Map();
|
|
514
|
+
function renderTests() {
|
|
515
|
+
clearEl(testsPaneEl);
|
|
516
|
+
if (options.runScenario === undefined) {
|
|
517
|
+
testsPaneEl.appendChild(emptyEl("Pass runScenario in options to enable the Tests tab."));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Run all button
|
|
521
|
+
const headerEl = document.createElement("div");
|
|
522
|
+
headerEl.className = "bpmnkit-runner-tests-header";
|
|
523
|
+
const runAllBtn = document.createElement("button");
|
|
524
|
+
runAllBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-run-all";
|
|
525
|
+
runAllBtn.textContent = `\u25B6 Run all (${scenarios.length})`;
|
|
526
|
+
runAllBtn.disabled = scenarios.length === 0;
|
|
527
|
+
runAllBtn.addEventListener("click", () => {
|
|
528
|
+
runAllBtn.disabled = true;
|
|
529
|
+
const runs = scenarios.map((s) => options.runScenario(s).then((r) => {
|
|
530
|
+
scenarioResults.set(s.id, r);
|
|
531
|
+
renderTests();
|
|
532
|
+
}));
|
|
533
|
+
void Promise.all(runs).then(() => renderTests());
|
|
534
|
+
});
|
|
535
|
+
headerEl.appendChild(runAllBtn);
|
|
536
|
+
const addBtn = document.createElement("button");
|
|
537
|
+
addBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-add";
|
|
538
|
+
addBtn.textContent = "+ New scenario";
|
|
539
|
+
addBtn.addEventListener("click", () => {
|
|
540
|
+
const id = `scenario-${Date.now()}`;
|
|
541
|
+
scenarios.push({
|
|
542
|
+
id,
|
|
543
|
+
name: `Scenario ${scenarios.length + 1}`,
|
|
544
|
+
inputs: {},
|
|
545
|
+
mocks: {},
|
|
546
|
+
expect: {},
|
|
547
|
+
});
|
|
548
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
549
|
+
renderTests();
|
|
550
|
+
});
|
|
551
|
+
headerEl.appendChild(addBtn);
|
|
552
|
+
// AI generate scenarios button
|
|
553
|
+
if (options.generateScenarios !== undefined) {
|
|
554
|
+
const genBtn = document.createElement("button");
|
|
555
|
+
genBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-gen";
|
|
556
|
+
genBtn.textContent = "\u2728 Generate";
|
|
557
|
+
genBtn.title = "Generate test scenarios with AI";
|
|
558
|
+
genBtn.addEventListener("click", () => {
|
|
559
|
+
genBtn.disabled = true;
|
|
560
|
+
genBtn.textContent = "Generating\u2026";
|
|
561
|
+
void options.generateScenarios()
|
|
562
|
+
.then((newScenarios) => {
|
|
563
|
+
scenarios.push(...newScenarios);
|
|
564
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
565
|
+
})
|
|
566
|
+
.catch(() => undefined)
|
|
567
|
+
.finally(() => renderTests());
|
|
568
|
+
});
|
|
569
|
+
headerEl.appendChild(genBtn);
|
|
570
|
+
}
|
|
571
|
+
// Import chaos findings as draft scenarios
|
|
572
|
+
if (lastChaosInjections.length > 0 && options.getJobType !== undefined) {
|
|
573
|
+
const importChaosBtn = document.createElement("button");
|
|
574
|
+
importChaosBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-chaos-import";
|
|
575
|
+
importChaosBtn.textContent = `\u2193 Chaos (${lastChaosInjections.length})`;
|
|
576
|
+
importChaosBtn.title = "Import chaos findings as draft test scenarios";
|
|
577
|
+
importChaosBtn.addEventListener("click", () => {
|
|
578
|
+
const newScenarios = [];
|
|
579
|
+
for (let idx = 0; idx < lastChaosInjections.length; idx++) {
|
|
580
|
+
const inj = lastChaosInjections[idx];
|
|
581
|
+
if (inj === undefined)
|
|
582
|
+
continue;
|
|
583
|
+
const jobType = options.getJobType?.(inj.elementId) ?? null;
|
|
584
|
+
if (jobType === null)
|
|
585
|
+
continue;
|
|
586
|
+
newScenarios.push({
|
|
587
|
+
id: `chaos-${Date.now()}-${idx}`,
|
|
588
|
+
name: `Chaos: ${inj.elementId} (${inj.type})`,
|
|
589
|
+
mocks: { [jobType]: { error: `[Chaos] ${inj.type}` } },
|
|
590
|
+
expect: {},
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
if (newScenarios.length > 0) {
|
|
594
|
+
scenarios.push(...newScenarios);
|
|
595
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
596
|
+
renderTests();
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
headerEl.appendChild(importChaosBtn);
|
|
600
|
+
}
|
|
601
|
+
testsPaneEl.appendChild(headerEl);
|
|
602
|
+
if (scenarios.length === 0) {
|
|
603
|
+
testsPaneEl.appendChild(emptyEl("No test scenarios yet. Click + New scenario."));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
for (let i = 0; i < scenarios.length; i++) {
|
|
607
|
+
const scenario = scenarios[i];
|
|
608
|
+
if (scenario === undefined)
|
|
609
|
+
continue;
|
|
610
|
+
const result = scenarioResults.get(scenario.id);
|
|
611
|
+
const rowEl = document.createElement("div");
|
|
612
|
+
rowEl.className = "bpmnkit-runner-tests-row";
|
|
613
|
+
if (result !== undefined) {
|
|
614
|
+
rowEl.classList.add(result.passed ? "bpmnkit-runner-tests-pass" : "bpmnkit-runner-tests-fail");
|
|
615
|
+
}
|
|
616
|
+
const statusEl = document.createElement("span");
|
|
617
|
+
statusEl.className = "bpmnkit-runner-tests-status";
|
|
618
|
+
statusEl.textContent = result === undefined ? "\u25CB" : result.passed ? "\u2713" : "\u2717";
|
|
619
|
+
const nameInput = document.createElement("input");
|
|
620
|
+
nameInput.className = "bpmnkit-runner-tests-name";
|
|
621
|
+
nameInput.value = scenario.name;
|
|
622
|
+
nameInput.addEventListener("input", () => {
|
|
623
|
+
if (scenarios[i] !== undefined) {
|
|
624
|
+
;
|
|
625
|
+
scenarios[i].name = nameInput.value;
|
|
626
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
const runOneBtn = document.createElement("button");
|
|
630
|
+
runOneBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-run-one";
|
|
631
|
+
runOneBtn.textContent = "\u25B6";
|
|
632
|
+
runOneBtn.title = "Run this scenario";
|
|
633
|
+
runOneBtn.addEventListener("click", () => {
|
|
634
|
+
void options.runScenario(scenario).then((r) => {
|
|
635
|
+
scenarioResults.set(scenario.id, r);
|
|
636
|
+
renderTests();
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
const delBtn = document.createElement("button");
|
|
640
|
+
delBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-del";
|
|
641
|
+
delBtn.textContent = "\u00D7";
|
|
642
|
+
delBtn.title = "Delete";
|
|
643
|
+
delBtn.addEventListener("click", () => {
|
|
644
|
+
scenarios.splice(i, 1);
|
|
645
|
+
scenarioResults.delete(scenario.id);
|
|
646
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
647
|
+
renderTests();
|
|
648
|
+
});
|
|
649
|
+
rowEl.appendChild(statusEl);
|
|
650
|
+
rowEl.appendChild(nameInput);
|
|
651
|
+
rowEl.appendChild(runOneBtn);
|
|
652
|
+
rowEl.appendChild(delBtn);
|
|
653
|
+
testsPaneEl.appendChild(rowEl);
|
|
654
|
+
// Expandable diff on failure
|
|
655
|
+
if (result !== undefined && !result.passed) {
|
|
656
|
+
const diffEl = document.createElement("div");
|
|
657
|
+
diffEl.className = "bpmnkit-runner-tests-diff";
|
|
658
|
+
for (const f of result.failures) {
|
|
659
|
+
const failRow = document.createElement("div");
|
|
660
|
+
failRow.className = "bpmnkit-runner-tests-diff-row";
|
|
661
|
+
failRow.textContent = `${f.field}: expected ${JSON.stringify(f.expected)}, got ${JSON.stringify(f.actual)}`;
|
|
662
|
+
diffEl.appendChild(failRow);
|
|
663
|
+
}
|
|
664
|
+
for (const e of result.errors) {
|
|
665
|
+
const errRow = document.createElement("div");
|
|
666
|
+
errRow.className = "bpmnkit-runner-tests-diff-row bpmnkit-runner-tests-diff-error";
|
|
667
|
+
errRow.textContent = `Error${e.elementId !== undefined ? ` (${e.elementId})` : ""}: ${e.message}`;
|
|
668
|
+
diffEl.appendChild(errRow);
|
|
669
|
+
}
|
|
670
|
+
testsPaneEl.appendChild(diffEl);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
309
674
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
310
675
|
function getPrimaryProcessId() {
|
|
311
676
|
return currentProcessId;
|
|
@@ -314,9 +679,14 @@ export function createProcessRunnerPlugin(options) {
|
|
|
314
679
|
feelEvals.length = 0;
|
|
315
680
|
variables.clear();
|
|
316
681
|
errors.length = 0;
|
|
682
|
+
eventLog.length = 0;
|
|
683
|
+
scrubIndex = null;
|
|
684
|
+
lastChaosInjections.length = 0;
|
|
685
|
+
lastChaosRunCompleted = null;
|
|
317
686
|
renderVariables();
|
|
318
687
|
renderFeelEvals();
|
|
319
688
|
renderErrors();
|
|
689
|
+
updateScrubber();
|
|
320
690
|
}
|
|
321
691
|
/** Cancel running instance and reset run state. Stays in play mode. */
|
|
322
692
|
function cleanup() {
|
|
@@ -345,6 +715,22 @@ export function createProcessRunnerPlugin(options) {
|
|
|
345
715
|
options.onExitPlayMode?.();
|
|
346
716
|
options.onHidePlayTab?.();
|
|
347
717
|
}
|
|
718
|
+
function applyChaosThenResolve(chaos, resolve, reject) {
|
|
719
|
+
switch (chaos.type) {
|
|
720
|
+
case "service-failure":
|
|
721
|
+
reject(new Error(`[Chaos] Simulated service failure at "${chaos.elementId}"`));
|
|
722
|
+
break;
|
|
723
|
+
case "null-response":
|
|
724
|
+
// Completes with empty variables — downstream FEEL will get nulls
|
|
725
|
+
resolve();
|
|
726
|
+
break;
|
|
727
|
+
case "random-delay": {
|
|
728
|
+
const delay = 500 + Math.random() * 2000;
|
|
729
|
+
setTimeout(resolve, delay);
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
348
734
|
function startInstance(vars, stepMode = false) {
|
|
349
735
|
if (currentInstance !== null)
|
|
350
736
|
cleanup();
|
|
@@ -352,23 +738,72 @@ export function createProcessRunnerPlugin(options) {
|
|
|
352
738
|
if (processId === undefined)
|
|
353
739
|
return;
|
|
354
740
|
mode = stepMode ? "running-step" : "running-auto";
|
|
741
|
+
// Build chaos schedule from flow elements of current process
|
|
742
|
+
if (chaosEnabled) {
|
|
743
|
+
// Gather element IDs from the current definitions
|
|
744
|
+
const defs = engine._defs;
|
|
745
|
+
const proc = defs?.processes?.find((p) => p.id === processId);
|
|
746
|
+
const eligibleIds = proc?.flowElements?.filter((e) => CHAOS_ELIGIBLE_TYPES.has(e.type)).map((e) => e.id) ?? [];
|
|
747
|
+
chaosSchedule = buildChaosSchedule(eligibleIds, CHAOS_FAILURE_PROBABILITY);
|
|
748
|
+
if (chaosSchedule.size > 0) {
|
|
749
|
+
errors.push({
|
|
750
|
+
message: `[Chaos] Scheduled injections for ${chaosSchedule.size} element(s): ${[...chaosSchedule.values()].map((i) => `${i.elementId}(${i.type})`).join(", ")}`,
|
|
751
|
+
});
|
|
752
|
+
renderErrors();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
chaosSchedule = new Map();
|
|
757
|
+
}
|
|
355
758
|
updateToolbar();
|
|
356
759
|
const beforeComplete = stepMode
|
|
357
|
-
? (
|
|
358
|
-
|
|
359
|
-
|
|
760
|
+
? (elementId) => new Promise((resolve, reject) => {
|
|
761
|
+
const chaos = chaosEnabled ? chaosSchedule.get(elementId) : undefined;
|
|
762
|
+
if (chaos !== undefined) {
|
|
763
|
+
lastChaosInjections.push(chaos);
|
|
764
|
+
applyChaosThenResolve(chaos, resolve, reject);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
stepQueue.push(resolve);
|
|
768
|
+
updateToolbar();
|
|
769
|
+
}
|
|
360
770
|
})
|
|
361
|
-
: (
|
|
362
|
-
|
|
771
|
+
: (elementId) => new Promise((resolve, reject) => {
|
|
772
|
+
const chaos = chaosEnabled ? chaosSchedule.get(elementId) : undefined;
|
|
773
|
+
if (chaos !== undefined) {
|
|
774
|
+
lastChaosInjections.push(chaos);
|
|
775
|
+
applyChaosThenResolve(chaos, resolve, reject);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
setTimeout(resolve, AUTO_PLAY_DELAY_MS);
|
|
779
|
+
}
|
|
363
780
|
});
|
|
781
|
+
// Reset chaos tracking for new run
|
|
782
|
+
lastChaosInjections.length = 0;
|
|
783
|
+
lastChaosRunCompleted = null;
|
|
784
|
+
// Reset event log for new run
|
|
785
|
+
eventLog.length = 0;
|
|
786
|
+
scrubIndex = null;
|
|
787
|
+
updateScrubber();
|
|
364
788
|
const instance = engine.start(processId, vars, { beforeComplete });
|
|
365
789
|
currentInstance = instance;
|
|
366
790
|
if (options.tokenHighlight !== undefined) {
|
|
367
791
|
stopTrackHighlight = options.tokenHighlight.api.trackInstance(instance);
|
|
368
792
|
}
|
|
369
793
|
instance.onChange((evt) => {
|
|
794
|
+
// Record every event (capped at MAX_EVENT_LOG)
|
|
795
|
+
if (eventLog.length < MAX_EVENT_LOG) {
|
|
796
|
+
eventLog.push(evt);
|
|
797
|
+
}
|
|
798
|
+
// Update scrubber max — only if in live mode
|
|
799
|
+
if (scrubIndex === null)
|
|
800
|
+
updateScrubber();
|
|
370
801
|
const type = evt.type;
|
|
371
802
|
if (type === "process:completed" || type === "process:failed") {
|
|
803
|
+
if (chaosEnabled) {
|
|
804
|
+
lastChaosRunCompleted = type === "process:completed";
|
|
805
|
+
renderErrors();
|
|
806
|
+
}
|
|
372
807
|
if (type === "process:failed") {
|
|
373
808
|
const error = evt.error;
|
|
374
809
|
if (typeof error === "string") {
|
|
@@ -440,6 +875,21 @@ export function createProcessRunnerPlugin(options) {
|
|
|
440
875
|
return;
|
|
441
876
|
const isRunning = mode !== "idle";
|
|
442
877
|
const hasPendingStep = mode === "running-step" && stepQueue.length > 0;
|
|
878
|
+
// Chaos toggle (only show when idle)
|
|
879
|
+
if (!isRunning) {
|
|
880
|
+
const chaosLabel = document.createElement("label");
|
|
881
|
+
chaosLabel.className = "bpmnkit-runner-chaos-label";
|
|
882
|
+
const chaosCheckbox = document.createElement("input");
|
|
883
|
+
chaosCheckbox.type = "checkbox";
|
|
884
|
+
chaosCheckbox.className = "bpmnkit-runner-chaos-checkbox";
|
|
885
|
+
chaosCheckbox.checked = chaosEnabled;
|
|
886
|
+
chaosCheckbox.addEventListener("change", () => {
|
|
887
|
+
chaosEnabled = chaosCheckbox.checked;
|
|
888
|
+
});
|
|
889
|
+
chaosLabel.appendChild(chaosCheckbox);
|
|
890
|
+
chaosLabel.appendChild(document.createTextNode(" Chaos"));
|
|
891
|
+
toolbarEl.appendChild(chaosLabel);
|
|
892
|
+
}
|
|
443
893
|
// Run button
|
|
444
894
|
const runBtn = btn("\u25B6 Run");
|
|
445
895
|
runBtn.disabled = isRunning;
|
|
@@ -494,10 +944,11 @@ export function createProcessRunnerPlugin(options) {
|
|
|
494
944
|
renderFeelEvals();
|
|
495
945
|
renderErrors();
|
|
496
946
|
renderInputVars();
|
|
947
|
+
renderTests();
|
|
497
948
|
if (options.playContainer !== undefined) {
|
|
498
949
|
options.playContainer.appendChild(playPanelEl);
|
|
499
950
|
}
|
|
500
|
-
// Load persisted input variables for the initial project
|
|
951
|
+
// Load persisted input variables and scenarios for the initial project
|
|
501
952
|
currentProjectId = options.getProjectId?.() ?? null;
|
|
502
953
|
void loadInputVars(currentProjectId).then((loaded) => {
|
|
503
954
|
inputVars.length = 0;
|
|
@@ -505,13 +956,31 @@ export function createProcessRunnerPlugin(options) {
|
|
|
505
956
|
inputVars.push(v);
|
|
506
957
|
renderInputVars();
|
|
507
958
|
});
|
|
959
|
+
void loadScenarios(currentProjectId).then((loaded) => {
|
|
960
|
+
scenarios.length = 0;
|
|
961
|
+
for (const s of loaded)
|
|
962
|
+
scenarios.push(s);
|
|
963
|
+
renderTests();
|
|
964
|
+
});
|
|
965
|
+
// Auto-load sidecar on initial mount
|
|
966
|
+
if (options.loadSidecarScenarios !== undefined) {
|
|
967
|
+
void options.loadSidecarScenarios().then((sidecar) => {
|
|
968
|
+
if (sidecar !== null && sidecar.length > 0) {
|
|
969
|
+
scenarios.length = 0;
|
|
970
|
+
for (const s of sidecar)
|
|
971
|
+
scenarios.push(s);
|
|
972
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
973
|
+
renderTests();
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
508
977
|
const onAny = api.on;
|
|
509
978
|
unsubs.push(api.on("diagram:load", (defs) => {
|
|
510
979
|
currentProcessId = defs.processes[0]?.id;
|
|
511
980
|
engine.deploy({ bpmn: defs });
|
|
512
981
|
if (currentInstance !== null)
|
|
513
982
|
cleanup();
|
|
514
|
-
// Reload input vars if the project changed
|
|
983
|
+
// Reload input vars and scenarios if the project changed
|
|
515
984
|
const pid = options.getProjectId?.() ?? null;
|
|
516
985
|
if (pid !== currentProjectId) {
|
|
517
986
|
currentProjectId = pid;
|
|
@@ -521,6 +990,26 @@ export function createProcessRunnerPlugin(options) {
|
|
|
521
990
|
inputVars.push(v);
|
|
522
991
|
renderInputVars();
|
|
523
992
|
});
|
|
993
|
+
void loadScenarios(pid).then((loaded) => {
|
|
994
|
+
scenarios.length = 0;
|
|
995
|
+
for (const s of loaded)
|
|
996
|
+
scenarios.push(s);
|
|
997
|
+
scenarioResults.clear();
|
|
998
|
+
renderTests();
|
|
999
|
+
});
|
|
1000
|
+
// Auto-load sidecar test file alongside BPMN
|
|
1001
|
+
if (options.loadSidecarScenarios !== undefined) {
|
|
1002
|
+
void options.loadSidecarScenarios().then((sidecar) => {
|
|
1003
|
+
if (sidecar !== null && sidecar.length > 0) {
|
|
1004
|
+
scenarios.length = 0;
|
|
1005
|
+
for (const s of sidecar)
|
|
1006
|
+
scenarios.push(s);
|
|
1007
|
+
scenarioResults.clear();
|
|
1008
|
+
void saveScenarios(currentProjectId, scenarios);
|
|
1009
|
+
renderTests();
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
524
1013
|
}
|
|
525
1014
|
updateToolbar();
|
|
526
1015
|
}), api.on("diagram:clear", () => {
|