@bpmnkit/plugins 0.0.19 → 0.0.23

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.
@@ -1,3 +1,4 @@
1
+ import { findValidationStructure, getValidationInputNames } from "@bpmnkit/core";
1
2
  import { injectProcessRunnerStyles } from "./css.js";
2
3
  // ── IndexedDB persistence for input variables ───────────────────────────────
3
4
  function openRunnerDb() {
@@ -122,6 +123,27 @@ export function createProcessRunnerPlugin(options) {
122
123
  let currentProcessId;
123
124
  /** The project ID used to scope input variable persistence. */
124
125
  let currentProjectId = null;
126
+ // Wrappers that prefer the caller-provided callbacks over internal IndexedDB.
127
+ async function persistScenarios(scenarios) {
128
+ if (options.onSaveScenarios)
129
+ return options.onSaveScenarios(scenarios);
130
+ return saveScenarios(currentProjectId, scenarios);
131
+ }
132
+ async function fetchScenarios() {
133
+ if (options.onLoadScenarios)
134
+ return options.onLoadScenarios();
135
+ return loadScenarios(currentProjectId);
136
+ }
137
+ async function persistInputVars(vars) {
138
+ if (options.onSaveInputVars)
139
+ return options.onSaveInputVars(vars);
140
+ return saveInputVars(currentProjectId, vars);
141
+ }
142
+ async function fetchInputVars() {
143
+ if (options.onLoadInputVars)
144
+ return options.onLoadInputVars();
145
+ return loadInputVars(currentProjectId);
146
+ }
125
147
  /** Pending step resolvers — each represents a paused beforeComplete call. */
126
148
  const stepQueue = [];
127
149
  /** Accumulated FEEL expression evaluations for the current run. */
@@ -146,6 +168,11 @@ export function createProcessRunnerPlugin(options) {
146
168
  let lastChaosRunCompleted = null;
147
169
  /** Input variables configured by the user (persisted in IndexedDB). */
148
170
  const inputVars = [];
171
+ /** Which scenario is open in the editor (null = list view). */
172
+ let editingScenarioId = null;
173
+ /** Element ID last clicked in the canvas while editing a scenario. */
174
+ let focusedElementId = null;
175
+ const MOCKABLE_TASK_TYPES = new Set(["serviceTask", "sendTask", "businessRuleTask", "userTask"]);
149
176
  const toolbarEl = document.createElement("div");
150
177
  toolbarEl.className = "bpmnkit-runner-toolbar";
151
178
  /** Entry button placed in the HUD action bar (styled by initEditorHud). */
@@ -170,12 +197,14 @@ export function createProcessRunnerPlugin(options) {
170
197
  const feelTabBtn = makeTabBtn("FEEL", false);
171
198
  const errorsTabBtn = makeTabBtn("Errors", false);
172
199
  const inputTabBtn = makeTabBtn("Input", false);
173
- const testsTabBtn = makeTabBtn("Tests", false);
200
+ // Tests sub-tab only shown when there is no dedicated testsContainer
201
+ const testsTabBtn = options.testsContainer === undefined ? makeTabBtn("Tests", false) : null;
174
202
  playTabBarEl.appendChild(varTabBtn);
175
203
  playTabBarEl.appendChild(feelTabBtn);
176
204
  playTabBarEl.appendChild(errorsTabBtn);
177
205
  playTabBarEl.appendChild(inputTabBtn);
178
- playTabBarEl.appendChild(testsTabBtn);
206
+ if (testsTabBtn !== null)
207
+ playTabBarEl.appendChild(testsTabBtn);
179
208
  function makePaneEl(hidden) {
180
209
  const d = document.createElement("div");
181
210
  d.className = hidden
@@ -217,24 +246,34 @@ export function createProcessRunnerPlugin(options) {
217
246
  playPanelEl.appendChild(feelPaneEl);
218
247
  playPanelEl.appendChild(errorsPaneEl);
219
248
  playPanelEl.appendChild(ivarsPaneEl);
220
- playPanelEl.appendChild(testsPaneEl);
249
+ // Mount tests pane into dedicated container when provided, otherwise keep as sub-tab
250
+ if (options.testsContainer !== undefined) {
251
+ testsPaneEl.classList.remove("bpmnkit-runner-play-pane--hidden");
252
+ options.testsContainer.appendChild(testsPaneEl);
253
+ }
254
+ else {
255
+ playPanelEl.appendChild(testsPaneEl);
256
+ }
221
257
  function switchPlayTab(tab) {
222
258
  varTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "variables");
223
259
  feelTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "feel");
224
260
  errorsTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "errors");
225
261
  inputTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "input");
226
- testsTabBtn.classList.toggle("bpmnkit-runner-play-tab--active", tab === "tests");
262
+ testsTabBtn?.classList.toggle("bpmnkit-runner-play-tab--active", tab === "tests");
227
263
  varsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "variables");
228
264
  feelPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "feel");
229
265
  errorsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "errors");
230
266
  ivarsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "input");
231
- testsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "tests");
267
+ // testsPaneEl lives in the dock when testsContainer is set — don't hide/show it here
268
+ if (options.testsContainer === undefined) {
269
+ testsPaneEl.classList.toggle("bpmnkit-runner-play-pane--hidden", tab !== "tests");
270
+ }
232
271
  }
233
272
  varTabBtn.addEventListener("click", () => switchPlayTab("variables"));
234
273
  feelTabBtn.addEventListener("click", () => switchPlayTab("feel"));
235
274
  errorsTabBtn.addEventListener("click", () => switchPlayTab("errors"));
236
275
  inputTabBtn.addEventListener("click", () => switchPlayTab("input"));
237
- testsTabBtn.addEventListener("click", () => switchPlayTab("tests"));
276
+ testsTabBtn?.addEventListener("click", () => switchPlayTab("tests"));
238
277
  function computeStateAt(idx) {
239
278
  const vars = new Map();
240
279
  const feels = [];
@@ -450,6 +489,25 @@ export function createProcessRunnerPlugin(options) {
450
489
  }
451
490
  function renderInputVars() {
452
491
  clearEl(ivarsPaneEl);
492
+ // ── Validation hints ────────────────────────────────────────────────────
493
+ if (options.getValidationDmn && options.getDefinitions) {
494
+ const defs = options.getDefinitions();
495
+ const startEvent = defs?.processes[0]?.flowElements.find((e) => e.type === "startEvent");
496
+ if (startEvent && defs) {
497
+ // getDefinitions returns a minimal interface; cast to BpmnDefinitions-compatible shape
498
+ const structure = findValidationStructure(defs, startEvent.id);
499
+ if (structure) {
500
+ const dmnXml = options.getValidationDmn(structure.decisionId);
501
+ const names = dmnXml ? getValidationInputNames(dmnXml) : [];
502
+ if (names.length > 0) {
503
+ const hintsEl = document.createElement("div");
504
+ hintsEl.className = "bpmnkit-runner-play-ivar-hints";
505
+ hintsEl.innerHTML = `<span class="bpmnkit-runner-play-ivar-hints-label">Expected:</span> ${names.map((n) => `<span class="bpmnkit-runner-play-ivar-hint-chip">${n}</span>`).join("")}`;
506
+ ivarsPaneEl.appendChild(hintsEl);
507
+ }
508
+ }
509
+ }
510
+ }
453
511
  for (let i = 0; i < inputVars.length; i++) {
454
512
  const entry = inputVars[i];
455
513
  if (entry === undefined)
@@ -464,7 +522,7 @@ export function createProcessRunnerPlugin(options) {
464
522
  const v = inputVars[i];
465
523
  if (v !== undefined) {
466
524
  v.name = nameInput.value;
467
- void saveInputVars(currentProjectId, inputVars);
525
+ void persistInputVars(inputVars);
468
526
  }
469
527
  });
470
528
  const eqEl = document.createElement("span");
@@ -478,7 +536,7 @@ export function createProcessRunnerPlugin(options) {
478
536
  const v = inputVars[i];
479
537
  if (v !== undefined) {
480
538
  v.value = valueInput.value;
481
- void saveInputVars(currentProjectId, inputVars);
539
+ void persistInputVars(inputVars);
482
540
  }
483
541
  });
484
542
  const delBtn = document.createElement("button");
@@ -487,7 +545,7 @@ export function createProcessRunnerPlugin(options) {
487
545
  delBtn.addEventListener("click", () => {
488
546
  inputVars.splice(i, 1);
489
547
  renderInputVars();
490
- void saveInputVars(currentProjectId, inputVars);
548
+ void persistInputVars(inputVars);
491
549
  });
492
550
  row.appendChild(nameInput);
493
551
  row.appendChild(eqEl);
@@ -501,23 +559,129 @@ export function createProcessRunnerPlugin(options) {
501
559
  addBtn.addEventListener("click", () => {
502
560
  inputVars.push({ name: "", value: "" });
503
561
  renderInputVars();
504
- void saveInputVars(currentProjectId, inputVars);
562
+ void persistInputVars(inputVars);
505
563
  // Focus the name field of the new row
506
564
  const rows = ivarsPaneEl.querySelectorAll(".bpmnkit-runner-play-ivar-name");
507
565
  rows[rows.length - 1]?.focus();
508
566
  });
509
567
  ivarsPaneEl.appendChild(addBtn);
510
568
  }
569
+ // ── Tests helpers ─────────────────────────────────────────────────────────
570
+ function parseVarValue(str) {
571
+ const t = str.trim();
572
+ if (!t)
573
+ return "";
574
+ try {
575
+ return JSON.parse(t);
576
+ }
577
+ catch {
578
+ return t;
579
+ }
580
+ }
581
+ function formatVarValue(val) {
582
+ return typeof val === "string" ? val : JSON.stringify(val);
583
+ }
584
+ function makeSectionTitle(text) {
585
+ const el = document.createElement("div");
586
+ el.className = "bpmnkit-runner-tests-section-title";
587
+ el.textContent = text;
588
+ return el;
589
+ }
590
+ /** Renders a key=value editor widget. Calls onUpdate whenever entries change. */
591
+ function makeVarList(vars, addLabel, onUpdate) {
592
+ const wrap = document.createElement("div");
593
+ wrap.className = "bpmnkit-runner-tests-varlist";
594
+ const entries = Object.entries(vars).map(([k, v]) => ({
595
+ key: k,
596
+ val: formatVarValue(v),
597
+ }));
598
+ function save() {
599
+ const r = {};
600
+ for (const e of entries) {
601
+ if (e.key.trim())
602
+ r[e.key.trim()] = parseVarValue(e.val);
603
+ }
604
+ onUpdate(r);
605
+ }
606
+ function renderList() {
607
+ clearEl(wrap);
608
+ for (let i = 0; i < entries.length; i++) {
609
+ const entry = entries[i];
610
+ if (entry === undefined)
611
+ continue;
612
+ const row = document.createElement("div");
613
+ row.className = "bpmnkit-runner-play-ivar-row";
614
+ const nameInput = document.createElement("input");
615
+ nameInput.className = "bpmnkit-runner-play-ivar-name";
616
+ nameInput.placeholder = "name";
617
+ nameInput.value = entry.key;
618
+ nameInput.addEventListener("input", () => {
619
+ if (entries[i] !== undefined) {
620
+ ;
621
+ entries[i].key = nameInput.value;
622
+ save();
623
+ }
624
+ });
625
+ const eq = document.createElement("span");
626
+ eq.className = "bpmnkit-runner-play-ivar-eq";
627
+ eq.textContent = "=";
628
+ const valInput = document.createElement("input");
629
+ valInput.className = "bpmnkit-runner-play-ivar-value";
630
+ valInput.placeholder = "value";
631
+ valInput.value = entry.val;
632
+ valInput.addEventListener("input", () => {
633
+ if (entries[i] !== undefined) {
634
+ ;
635
+ entries[i].val = valInput.value;
636
+ save();
637
+ }
638
+ });
639
+ const del = document.createElement("button");
640
+ del.className = "bpmnkit-runner-play-ivar-del";
641
+ del.textContent = "\u00D7";
642
+ del.addEventListener("click", () => {
643
+ entries.splice(i, 1);
644
+ renderList();
645
+ save();
646
+ });
647
+ row.appendChild(nameInput);
648
+ row.appendChild(eq);
649
+ row.appendChild(valInput);
650
+ row.appendChild(del);
651
+ wrap.appendChild(row);
652
+ }
653
+ const addBtn = document.createElement("button");
654
+ addBtn.className = "bpmnkit-runner-play-ivar-add";
655
+ addBtn.textContent = addLabel;
656
+ addBtn.addEventListener("click", () => {
657
+ entries.push({ key: "", val: "" });
658
+ renderList();
659
+ const inputs = wrap.querySelectorAll(".bpmnkit-runner-play-ivar-name");
660
+ inputs[inputs.length - 1]?.focus();
661
+ });
662
+ wrap.appendChild(addBtn);
663
+ }
664
+ renderList();
665
+ return wrap;
666
+ }
511
667
  // ── Tests tab ────────────────────────────────────────────────────────────
512
668
  const scenarios = [];
513
669
  const scenarioResults = new Map();
514
670
  function renderTests() {
671
+ if (editingScenarioId !== null) {
672
+ renderScenarioEditor();
673
+ }
674
+ else {
675
+ renderScenarioList();
676
+ }
677
+ }
678
+ function renderScenarioList() {
515
679
  clearEl(testsPaneEl);
516
680
  if (options.runScenario === undefined) {
517
681
  testsPaneEl.appendChild(emptyEl("Pass runScenario in options to enable the Tests tab."));
518
682
  return;
519
683
  }
520
- // Run all button
684
+ // Header
521
685
  const headerEl = document.createElement("div");
522
686
  headerEl.className = "bpmnkit-runner-tests-header";
523
687
  const runAllBtn = document.createElement("button");
@@ -535,7 +699,7 @@ export function createProcessRunnerPlugin(options) {
535
699
  headerEl.appendChild(runAllBtn);
536
700
  const addBtn = document.createElement("button");
537
701
  addBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-add";
538
- addBtn.textContent = "+ New scenario";
702
+ addBtn.textContent = "+ New";
539
703
  addBtn.addEventListener("click", () => {
540
704
  const id = `scenario-${Date.now()}`;
541
705
  scenarios.push({
@@ -545,11 +709,12 @@ export function createProcessRunnerPlugin(options) {
545
709
  mocks: {},
546
710
  expect: {},
547
711
  });
548
- void saveScenarios(currentProjectId, scenarios);
712
+ void persistScenarios(scenarios);
713
+ editingScenarioId = id;
714
+ focusedElementId = null;
549
715
  renderTests();
550
716
  });
551
717
  headerEl.appendChild(addBtn);
552
- // AI generate scenarios button
553
718
  if (options.generateScenarios !== undefined) {
554
719
  const genBtn = document.createElement("button");
555
720
  genBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-gen";
@@ -561,14 +726,13 @@ export function createProcessRunnerPlugin(options) {
561
726
  void options.generateScenarios()
562
727
  .then((newScenarios) => {
563
728
  scenarios.push(...newScenarios);
564
- void saveScenarios(currentProjectId, scenarios);
729
+ void persistScenarios(scenarios);
565
730
  })
566
731
  .catch(() => undefined)
567
732
  .finally(() => renderTests());
568
733
  });
569
734
  headerEl.appendChild(genBtn);
570
735
  }
571
- // Import chaos findings as draft scenarios
572
736
  if (lastChaosInjections.length > 0 && options.getJobType !== undefined) {
573
737
  const importChaosBtn = document.createElement("button");
574
738
  importChaosBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-chaos-import";
@@ -592,7 +756,7 @@ export function createProcessRunnerPlugin(options) {
592
756
  }
593
757
  if (newScenarios.length > 0) {
594
758
  scenarios.push(...newScenarios);
595
- void saveScenarios(currentProjectId, scenarios);
759
+ void persistScenarios(scenarios);
596
760
  renderTests();
597
761
  }
598
762
  });
@@ -600,7 +764,7 @@ export function createProcessRunnerPlugin(options) {
600
764
  }
601
765
  testsPaneEl.appendChild(headerEl);
602
766
  if (scenarios.length === 0) {
603
- testsPaneEl.appendChild(emptyEl("No test scenarios yet. Click + New scenario."));
767
+ testsPaneEl.appendChild(emptyEl("No test scenarios yet. Click + New."));
604
768
  return;
605
769
  }
606
770
  for (let i = 0; i < scenarios.length; i++) {
@@ -616,15 +780,17 @@ export function createProcessRunnerPlugin(options) {
616
780
  const statusEl = document.createElement("span");
617
781
  statusEl.className = "bpmnkit-runner-tests-status";
618
782
  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
- }
783
+ const nameEl = document.createElement("span");
784
+ nameEl.className = "bpmnkit-runner-tests-name-label";
785
+ nameEl.textContent = scenario.name;
786
+ const editBtn = document.createElement("button");
787
+ editBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-edit";
788
+ editBtn.textContent = "\u270E";
789
+ editBtn.title = "Edit scenario";
790
+ editBtn.addEventListener("click", () => {
791
+ editingScenarioId = scenario.id;
792
+ focusedElementId = null;
793
+ renderTests();
628
794
  });
629
795
  const runOneBtn = document.createElement("button");
630
796
  runOneBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-run-one";
@@ -643,15 +809,16 @@ export function createProcessRunnerPlugin(options) {
643
809
  delBtn.addEventListener("click", () => {
644
810
  scenarios.splice(i, 1);
645
811
  scenarioResults.delete(scenario.id);
646
- void saveScenarios(currentProjectId, scenarios);
812
+ void persistScenarios(scenarios);
647
813
  renderTests();
648
814
  });
649
815
  rowEl.appendChild(statusEl);
650
- rowEl.appendChild(nameInput);
816
+ rowEl.appendChild(nameEl);
817
+ rowEl.appendChild(editBtn);
651
818
  rowEl.appendChild(runOneBtn);
652
819
  rowEl.appendChild(delBtn);
653
820
  testsPaneEl.appendChild(rowEl);
654
- // Expandable diff on failure
821
+ // Failure diff
655
822
  if (result !== undefined && !result.passed) {
656
823
  const diffEl = document.createElement("div");
657
824
  diffEl.className = "bpmnkit-runner-tests-diff";
@@ -671,6 +838,310 @@ export function createProcessRunnerPlugin(options) {
671
838
  }
672
839
  }
673
840
  }
841
+ function renderScenarioEditor() {
842
+ clearEl(testsPaneEl);
843
+ const scenario = scenarios.find((s) => s.id === editingScenarioId);
844
+ if (scenario === undefined) {
845
+ editingScenarioId = null;
846
+ renderScenarioList();
847
+ return;
848
+ }
849
+ const result = scenarioResults.get(scenario.id);
850
+ // ── Editor header ───────────────────────────────────────────────────────
851
+ const headerEl = document.createElement("div");
852
+ headerEl.className = "bpmnkit-runner-tests-editor-header";
853
+ const backBtn = document.createElement("button");
854
+ backBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-back";
855
+ backBtn.textContent = "\u2190 Back";
856
+ backBtn.addEventListener("click", () => {
857
+ editingScenarioId = null;
858
+ focusedElementId = null;
859
+ renderTests();
860
+ });
861
+ headerEl.appendChild(backBtn);
862
+ const nameInput = document.createElement("input");
863
+ nameInput.className = "bpmnkit-runner-tests-editor-name";
864
+ nameInput.value = scenario.name;
865
+ nameInput.placeholder = "Scenario name";
866
+ nameInput.addEventListener("input", () => {
867
+ scenario.name = nameInput.value;
868
+ void persistScenarios(scenarios);
869
+ });
870
+ headerEl.appendChild(nameInput);
871
+ const runBtn = document.createElement("button");
872
+ runBtn.className = "bpmnkit-runner-btn bpmnkit-runner-tests-run-one";
873
+ runBtn.textContent = "\u25B6 Run";
874
+ runBtn.title = "Run this scenario";
875
+ if (options.runScenario !== undefined) {
876
+ runBtn.addEventListener("click", () => {
877
+ runBtn.disabled = true;
878
+ void options.runScenario(scenario).then((r) => {
879
+ scenarioResults.set(scenario.id, r);
880
+ renderTests();
881
+ });
882
+ });
883
+ }
884
+ else {
885
+ runBtn.disabled = true;
886
+ }
887
+ headerEl.appendChild(runBtn);
888
+ if (result !== undefined) {
889
+ const badge = document.createElement("span");
890
+ badge.className = `bpmnkit-runner-tests-editor-badge ${result.passed ? "bpmnkit-runner-tests-editor-badge--pass" : "bpmnkit-runner-tests-editor-badge--fail"}`;
891
+ badge.textContent = result.passed ? "\u2713 passed" : "\u2717 failed";
892
+ headerEl.appendChild(badge);
893
+ }
894
+ testsPaneEl.appendChild(headerEl);
895
+ // ── Start Variables ──────────────────────────────────────────────────────
896
+ testsPaneEl.appendChild(makeSectionTitle("Start Variables"));
897
+ testsPaneEl.appendChild(makeVarList(scenario.inputs ?? {}, "+ Add variable", (updated) => {
898
+ scenario.inputs = updated;
899
+ void persistScenarios(scenarios);
900
+ }));
901
+ // ── Task Mocks ──────────────────────────────────────────────────────────
902
+ const defs = options.getDefinitions?.() ?? null;
903
+ // Decisions referenced by BRTs — used both for filtering mocks and for missing-DMN warnings
904
+ const referencedDecisions = defs?.processes.flatMap((p) => p.flowElements.flatMap((e) => e.type === "businessRuleTask" && e.decisionId ? [e.decisionId] : [])) ?? [];
905
+ const missingDmns = options.getValidationDmn
906
+ ? referencedDecisions.filter((id) => options.getValidationDmn?.(id) === null)
907
+ : [];
908
+ const mockableTasks = defs?.processes.flatMap((p) => p.flowElements.filter((e) => {
909
+ if (!MOCKABLE_TASK_TYPES.has(e.type))
910
+ return false;
911
+ // BRTs with calledDecision run internally via DMN — no external job, no mock needed
912
+ if (e.type === "businessRuleTask" && e.decisionId)
913
+ return false;
914
+ return true;
915
+ })) ?? [];
916
+ if (mockableTasks.length > 0) {
917
+ testsPaneEl.appendChild(makeSectionTitle("Task Outputs"));
918
+ const hintEl = document.createElement("div");
919
+ hintEl.className = "bpmnkit-runner-tests-hint";
920
+ hintEl.textContent = "Click a task in the diagram to configure its mock output.";
921
+ testsPaneEl.appendChild(hintEl);
922
+ for (const task of mockableTasks) {
923
+ const jobType = options.getJobType?.(task.id) ?? task.id;
924
+ const mock = scenario.mocks?.[jobType] ?? {};
925
+ const isFocused = focusedElementId === task.id;
926
+ const taskEl = document.createElement("div");
927
+ taskEl.className = `bpmnkit-runner-tests-task${isFocused ? " bpmnkit-runner-tests-task--focused" : ""}`;
928
+ taskEl.dataset.elementId = task.id;
929
+ const taskHeaderEl = document.createElement("div");
930
+ taskHeaderEl.className = "bpmnkit-runner-tests-task-header";
931
+ const taskNameEl = document.createElement("span");
932
+ taskNameEl.className = "bpmnkit-runner-tests-task-name";
933
+ taskNameEl.textContent = task.name ?? task.id;
934
+ const typeEl = document.createElement("span");
935
+ typeEl.className = "bpmnkit-runner-tests-task-badge";
936
+ typeEl.textContent = task.type.replace("Task", "");
937
+ taskHeaderEl.appendChild(taskNameEl);
938
+ taskHeaderEl.appendChild(typeEl);
939
+ taskEl.appendChild(taskHeaderEl);
940
+ // Click header to focus/unfocus
941
+ taskHeaderEl.addEventListener("click", () => {
942
+ focusedElementId = isFocused ? null : task.id;
943
+ renderTests();
944
+ });
945
+ if (isFocused) {
946
+ const bodyEl = document.createElement("div");
947
+ bodyEl.className = "bpmnkit-runner-tests-task-body";
948
+ // Output variables
949
+ bodyEl.appendChild(makeVarList(mock.outputs ?? {}, "+ Add output", (updated) => {
950
+ if (scenario.mocks === undefined)
951
+ scenario.mocks = {};
952
+ scenario.mocks[jobType] = { ...mock, outputs: updated };
953
+ void persistScenarios(scenarios);
954
+ }));
955
+ // Error field
956
+ const errorRow = document.createElement("div");
957
+ errorRow.className = "bpmnkit-runner-tests-error-row";
958
+ const errorLabel = document.createElement("label");
959
+ errorLabel.className = "bpmnkit-runner-tests-error-label";
960
+ errorLabel.textContent = "Fail with error:";
961
+ const errorInput = document.createElement("input");
962
+ errorInput.className = "bpmnkit-runner-tests-error-input";
963
+ errorInput.placeholder = "error message (leave blank to complete)";
964
+ errorInput.value = mock.error ?? "";
965
+ errorInput.addEventListener("input", () => {
966
+ if (scenario.mocks === undefined)
967
+ scenario.mocks = {};
968
+ const err = errorInput.value.trim() || undefined;
969
+ scenario.mocks[jobType] = { ...mock, error: err };
970
+ void persistScenarios(scenarios);
971
+ });
972
+ errorRow.appendChild(errorLabel);
973
+ errorRow.appendChild(errorInput);
974
+ bodyEl.appendChild(errorRow);
975
+ taskEl.appendChild(bodyEl);
976
+ }
977
+ testsPaneEl.appendChild(taskEl);
978
+ }
979
+ }
980
+ // ── Missing DMN warning ──────────────────────────────────────────────────
981
+ if (missingDmns.length > 0) {
982
+ const warnEl = document.createElement("div");
983
+ warnEl.className = "bpmnkit-runner-tests-missing-dmn";
984
+ warnEl.textContent = `⚠ Decision model${missingDmns.length > 1 ? "s" : ""} not found: ${missingDmns.join(", ")}. Import the DMN in the Models view.`;
985
+ testsPaneEl.appendChild(warnEl);
986
+ }
987
+ // ── Expected Variables ──────────────────────────────────────────────────
988
+ testsPaneEl.appendChild(makeSectionTitle("Expected Variables"));
989
+ testsPaneEl.appendChild(makeVarList(scenario.expect?.variables ?? {}, "+ Add assertion", (updated) => {
990
+ if (scenario.expect === undefined)
991
+ scenario.expect = {};
992
+ scenario.expect.variables = updated;
993
+ void persistScenarios(scenarios);
994
+ }));
995
+ // ── Failure details ─────────────────────────────────────────────────────
996
+ if (result !== undefined && (result.failures.length > 0 || result.errors.length > 0)) {
997
+ testsPaneEl.appendChild(makeSectionTitle("Last Run Failures"));
998
+ const diffEl = document.createElement("div");
999
+ diffEl.className = "bpmnkit-runner-tests-diff";
1000
+ for (const f of result.failures) {
1001
+ const row = document.createElement("div");
1002
+ row.className = "bpmnkit-runner-tests-diff-row";
1003
+ row.textContent = `${f.field}: expected ${JSON.stringify(f.expected)}, got ${JSON.stringify(f.actual)}`;
1004
+ diffEl.appendChild(row);
1005
+ }
1006
+ for (const e of result.errors) {
1007
+ const row = document.createElement("div");
1008
+ row.className = "bpmnkit-runner-tests-diff-row bpmnkit-runner-tests-diff-error";
1009
+ row.textContent = `Error${e.elementId !== undefined ? ` (${e.elementId})` : ""}: ${e.message}`;
1010
+ diffEl.appendChild(row);
1011
+ }
1012
+ testsPaneEl.appendChild(diffEl);
1013
+ }
1014
+ // ── Last Run Trace ───────────────────────────────────────────────────────
1015
+ if (result !== undefined) {
1016
+ const traceResult = result;
1017
+ testsPaneEl.appendChild(makeSectionTitle("Last Run Trace"));
1018
+ const traceTabsEl = document.createElement("div");
1019
+ traceTabsEl.className = "bpmnkit-runner-play-tabs bpmnkit-runner-tests-trace-tabs";
1020
+ const tracePaneEl = document.createElement("div");
1021
+ tracePaneEl.className = "bpmnkit-runner-tests-trace-pane";
1022
+ let activeTraceTab = "vars";
1023
+ const tVarBtn = makeTabBtn("Variables", true);
1024
+ const tFeelBtn = makeTabBtn("FEEL", false);
1025
+ const tElemBtn = makeTabBtn("Elements", false);
1026
+ function renderTraceTab() {
1027
+ clearEl(tracePaneEl);
1028
+ tVarBtn.className =
1029
+ activeTraceTab === "vars"
1030
+ ? "bpmnkit-runner-play-tab bpmnkit-runner-play-tab--active"
1031
+ : "bpmnkit-runner-play-tab";
1032
+ tFeelBtn.className =
1033
+ activeTraceTab === "feel"
1034
+ ? "bpmnkit-runner-play-tab bpmnkit-runner-play-tab--active"
1035
+ : "bpmnkit-runner-play-tab";
1036
+ tElemBtn.className =
1037
+ activeTraceTab === "elements"
1038
+ ? "bpmnkit-runner-play-tab bpmnkit-runner-play-tab--active"
1039
+ : "bpmnkit-runner-play-tab";
1040
+ if (activeTraceTab === "vars") {
1041
+ const entries = Object.entries(traceResult.finalVariables);
1042
+ if (entries.length === 0) {
1043
+ tracePaneEl.appendChild(emptyEl("No variables."));
1044
+ }
1045
+ else {
1046
+ for (const [name, value] of entries) {
1047
+ const row = document.createElement("div");
1048
+ row.className = "bpmnkit-runner-play-var-row";
1049
+ const nameEl = document.createElement("span");
1050
+ nameEl.className = "bpmnkit-runner-play-var-name";
1051
+ nameEl.textContent = name;
1052
+ const valueEl = document.createElement("span");
1053
+ valueEl.className = "bpmnkit-runner-play-var-value";
1054
+ valueEl.textContent = JSON.stringify(value);
1055
+ row.append(nameEl, valueEl);
1056
+ tracePaneEl.appendChild(row);
1057
+ }
1058
+ }
1059
+ }
1060
+ else if (activeTraceTab === "feel") {
1061
+ const evals = traceResult.feelEvals;
1062
+ if (evals.length === 0) {
1063
+ tracePaneEl.appendChild(emptyEl("No FEEL evaluations recorded."));
1064
+ }
1065
+ else {
1066
+ const groups = new Map();
1067
+ for (const ev of evals) {
1068
+ let arr = groups.get(ev.elementId);
1069
+ if (arr === undefined) {
1070
+ arr = [];
1071
+ groups.set(ev.elementId, arr);
1072
+ }
1073
+ arr.push({ property: ev.property, expression: ev.expression, result: ev.result });
1074
+ }
1075
+ for (const [elementId, evArr] of groups) {
1076
+ const groupEl = document.createElement("div");
1077
+ groupEl.className = "bpmnkit-runner-play-feel-group";
1078
+ const headerEl = document.createElement("div");
1079
+ headerEl.className = "bpmnkit-runner-play-feel-header";
1080
+ headerEl.textContent = elementId;
1081
+ groupEl.appendChild(headerEl);
1082
+ for (const ev of evArr) {
1083
+ const rowEl = document.createElement("div");
1084
+ rowEl.className = "bpmnkit-runner-play-feel-row";
1085
+ const propEl = document.createElement("div");
1086
+ propEl.className = "bpmnkit-runner-play-feel-prop";
1087
+ propEl.textContent = ev.property;
1088
+ const exprEl = document.createElement("code");
1089
+ exprEl.className = "bpmnkit-runner-play-feel-expr";
1090
+ exprEl.textContent = ev.expression;
1091
+ const resultRowEl = document.createElement("div");
1092
+ resultRowEl.className = "bpmnkit-runner-play-feel-result-row";
1093
+ const arrowEl = document.createElement("span");
1094
+ arrowEl.className = "bpmnkit-runner-play-feel-arrow";
1095
+ arrowEl.textContent = "\u2192";
1096
+ const resultEl = document.createElement("span");
1097
+ resultEl.className = "bpmnkit-runner-play-feel-result";
1098
+ resultEl.textContent = JSON.stringify(ev.result);
1099
+ resultRowEl.append(arrowEl, resultEl);
1100
+ rowEl.append(propEl, exprEl, resultRowEl);
1101
+ groupEl.appendChild(rowEl);
1102
+ }
1103
+ tracePaneEl.appendChild(groupEl);
1104
+ }
1105
+ }
1106
+ }
1107
+ else {
1108
+ const elems = traceResult.visitedElements;
1109
+ if (elems.length === 0) {
1110
+ tracePaneEl.appendChild(emptyEl("No elements visited."));
1111
+ }
1112
+ else {
1113
+ for (let i = 0; i < elems.length; i++) {
1114
+ const row = document.createElement("div");
1115
+ row.className = "bpmnkit-runner-tests-trace-elem-row";
1116
+ const idxEl = document.createElement("span");
1117
+ idxEl.className = "bpmnkit-runner-tests-trace-elem-idx";
1118
+ idxEl.textContent = String(i + 1);
1119
+ const idEl = document.createElement("span");
1120
+ idEl.className = "bpmnkit-runner-tests-trace-elem-id";
1121
+ idEl.textContent = elems[i] ?? "";
1122
+ row.append(idxEl, idEl);
1123
+ tracePaneEl.appendChild(row);
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ tVarBtn.addEventListener("click", () => {
1129
+ activeTraceTab = "vars";
1130
+ renderTraceTab();
1131
+ });
1132
+ tFeelBtn.addEventListener("click", () => {
1133
+ activeTraceTab = "feel";
1134
+ renderTraceTab();
1135
+ });
1136
+ tElemBtn.addEventListener("click", () => {
1137
+ activeTraceTab = "elements";
1138
+ renderTraceTab();
1139
+ });
1140
+ traceTabsEl.append(tVarBtn, tFeelBtn, tElemBtn);
1141
+ renderTraceTab();
1142
+ testsPaneEl.append(traceTabsEl, tracePaneEl);
1143
+ }
1144
+ }
674
1145
  // ── Helpers ────────────────────────────────────────────────────────────
675
1146
  function getPrimaryProcessId() {
676
1147
  return currentProcessId;
@@ -950,13 +1421,13 @@ export function createProcessRunnerPlugin(options) {
950
1421
  }
951
1422
  // Load persisted input variables and scenarios for the initial project
952
1423
  currentProjectId = options.getProjectId?.() ?? null;
953
- void loadInputVars(currentProjectId).then((loaded) => {
1424
+ void fetchInputVars().then((loaded) => {
954
1425
  inputVars.length = 0;
955
1426
  for (const v of loaded)
956
1427
  inputVars.push(v);
957
1428
  renderInputVars();
958
1429
  });
959
- void loadScenarios(currentProjectId).then((loaded) => {
1430
+ void fetchScenarios().then((loaded) => {
960
1431
  scenarios.length = 0;
961
1432
  for (const s of loaded)
962
1433
  scenarios.push(s);
@@ -969,7 +1440,7 @@ export function createProcessRunnerPlugin(options) {
969
1440
  scenarios.length = 0;
970
1441
  for (const s of sidecar)
971
1442
  scenarios.push(s);
972
- void saveScenarios(currentProjectId, scenarios);
1443
+ void persistScenarios(scenarios);
973
1444
  renderTests();
974
1445
  }
975
1446
  });
@@ -984,13 +1455,13 @@ export function createProcessRunnerPlugin(options) {
984
1455
  const pid = options.getProjectId?.() ?? null;
985
1456
  if (pid !== currentProjectId) {
986
1457
  currentProjectId = pid;
987
- void loadInputVars(pid).then((loaded) => {
1458
+ void fetchInputVars().then((loaded) => {
988
1459
  inputVars.length = 0;
989
1460
  for (const v of loaded)
990
1461
  inputVars.push(v);
991
1462
  renderInputVars();
992
1463
  });
993
- void loadScenarios(pid).then((loaded) => {
1464
+ void fetchScenarios().then((loaded) => {
994
1465
  scenarios.length = 0;
995
1466
  for (const s of loaded)
996
1467
  scenarios.push(s);
@@ -1005,7 +1476,7 @@ export function createProcessRunnerPlugin(options) {
1005
1476
  for (const s of sidecar)
1006
1477
  scenarios.push(s);
1007
1478
  scenarioResults.clear();
1008
- void saveScenarios(currentProjectId, scenarios);
1479
+ void persistScenarios(scenarios);
1009
1480
  renderTests();
1010
1481
  }
1011
1482
  });
@@ -1023,6 +1494,18 @@ export function createProcessRunnerPlugin(options) {
1023
1494
  engine.deploy({ bpmn: defs });
1024
1495
  if (currentInstance !== null)
1025
1496
  cleanup();
1497
+ }), onAny("element:click", (evt) => {
1498
+ if (editingScenarioId === null)
1499
+ return;
1500
+ const typed = evt;
1501
+ const elementId = typed.element?.id;
1502
+ if (elementId === undefined)
1503
+ return;
1504
+ const elementType = typed.element?.type ?? "";
1505
+ if (!MOCKABLE_TASK_TYPES.has(elementType))
1506
+ return;
1507
+ focusedElementId = focusedElementId === elementId ? null : elementId;
1508
+ renderTests();
1026
1509
  }));
1027
1510
  },
1028
1511
  uninstall() {