@elabs-ai/components-process 4.2.0 → 5.0.0
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 +8 -1
- package/dist/core/index.d.ts +801 -3
- package/dist/core/index.js +1334 -0
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +1889 -34
- package/dist/index.js +5512 -196
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.ts +223 -5
- package/dist/test/index.js +346 -191
- package/dist/test/index.js.map +1 -1
- package/package.json +14 -13
- package/src/__contract__/case-table.contract.test.tsx +49 -0
- package/src/__contract__/compare-kpi-strip.contract.test.tsx +49 -0
- package/src/__contract__/conformance-overlay.contract.test.tsx +49 -0
- package/src/__contract__/happy-path-editor.contract.test.tsx +49 -0
- package/src/__contract__/violation-list.contract.test.tsx +49 -0
- package/src/abstraction-controls/abstraction-controls-per-type.test.tsx +80 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +43 -1
- package/src/abstraction-controls/abstraction-controls.tsx +198 -5
- package/src/case-table/case-table.stories.tsx +89 -0
- package/src/case-table/case-table.test.tsx +148 -0
- package/src/case-table/case-table.tsx +144 -0
- package/src/case-table/columns.ts +116 -0
- package/src/case-table/index.ts +11 -0
- package/src/case-timeline/case-timeline-model.test.ts +72 -0
- package/src/case-timeline/case-timeline-model.ts +112 -0
- package/src/case-timeline/case-timeline.stories.tsx +94 -0
- package/src/case-timeline/case-timeline.test.tsx +51 -0
- package/src/case-timeline/case-timeline.tsx +109 -0
- package/src/case-timeline/index.ts +9 -0
- package/src/conformance-overlay/conformance-fixture.ts +59 -0
- package/src/conformance-overlay/conformance-legend.tsx +109 -0
- package/src/conformance-overlay/conformance-overlay.stories.tsx +116 -0
- package/src/conformance-overlay/conformance-overlay.test.tsx +88 -0
- package/src/conformance-overlay/conformance-overlay.tsx +107 -0
- package/src/conformance-overlay/conformance-state.test.ts +79 -0
- package/src/conformance-overlay/conformance-state.ts +220 -0
- package/src/conformance-overlay/index.ts +4 -0
- package/src/core/activity-color-scale.test.ts +107 -0
- package/src/core/activity-color-scale.ts +133 -0
- package/src/core/adapters/ocel.test.ts +112 -0
- package/src/core/adapters/ocel.ts +359 -0
- package/src/core/adapters/xes.test.ts +293 -0
- package/src/core/adapters/xes.ts +384 -0
- package/src/core/cases-from-log.test.ts +72 -0
- package/src/core/cases-from-log.ts +85 -0
- package/src/core/conformance.test.ts +80 -0
- package/src/core/conformance.ts +91 -0
- package/src/core/diff-graphs.test.ts +151 -0
- package/src/core/diff-graphs.ts +118 -0
- package/src/core/discover-object-centric-graph.test.ts +94 -0
- package/src/core/discover-object-centric-graph.ts +296 -0
- package/src/core/fixtures/ocel-sample.ts +82 -0
- package/src/core/fixtures/sample.xes +68 -0
- package/src/core/index.ts +113 -0
- package/src/core/reference-model.test.ts +43 -0
- package/src/core/reference-model.ts +116 -0
- package/src/core/replay-timeline.test.ts +161 -0
- package/src/core/replay-timeline.ts +260 -0
- package/src/core/segments.test.ts +185 -0
- package/src/core/segments.ts +153 -0
- package/src/core/token-replay.test.ts +218 -0
- package/src/core/token-replay.ts +456 -0
- package/src/core/types.ts +2 -2
- package/src/dotted-chart/compute-dots.test.ts +176 -0
- package/src/dotted-chart/compute-dots.ts +241 -0
- package/src/dotted-chart/dotted-chart-labels.ts +93 -0
- package/src/dotted-chart/dotted-chart.stories.tsx +182 -0
- package/src/dotted-chart/dotted-chart.test.tsx +135 -0
- package/src/dotted-chart/dotted-chart.tsx +841 -0
- package/src/dotted-chart/index.ts +23 -0
- package/src/dotted-chart/use-element-size.ts +33 -0
- package/src/happy-path-editor/happy-path-editor-context.ts +81 -0
- package/src/happy-path-editor/happy-path-editor.stories.tsx +116 -0
- package/src/happy-path-editor/happy-path-editor.test.tsx +142 -0
- package/src/happy-path-editor/happy-path-editor.tsx +239 -0
- package/src/happy-path-editor/happy-path-step-node.tsx +175 -0
- package/src/happy-path-editor/index.ts +4 -0
- package/src/index.ts +51 -1
- package/src/performance-spectrum/aggregate-segments.test.ts +107 -0
- package/src/performance-spectrum/aggregate-segments.ts +174 -0
- package/src/performance-spectrum/index.ts +25 -0
- package/src/performance-spectrum/performance-spectrum-context.tsx +116 -0
- package/src/performance-spectrum/performance-spectrum.stories.tsx +128 -0
- package/src/performance-spectrum/performance-spectrum.test.tsx +190 -0
- package/src/performance-spectrum/performance-spectrum.tsx +870 -0
- package/src/process-compare/compare-kpi-strip.stories.tsx +48 -0
- package/src/process-compare/compare-kpi-strip.tsx +94 -0
- package/src/process-compare/compare-model.ts +83 -0
- package/src/process-compare/compare-side.tsx +42 -0
- package/src/process-compare/diff-to-graph.ts +104 -0
- package/src/process-compare/index.ts +23 -0
- package/src/process-compare/process-compare.stories.tsx +184 -0
- package/src/process-compare/process-compare.test.tsx +224 -0
- package/src/process-compare/process-compare.tsx +251 -0
- package/src/process-explorer.stories.tsx +1 -1
- package/src/process-filter-bar/index.ts +2 -0
- package/src/process-filter-bar/process-filter-bar.stories.tsx +156 -0
- package/src/process-filter-bar/process-filter-bar.test.tsx +201 -0
- package/src/process-filter-bar/process-filter-bar.tsx +167 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +47 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +67 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +148 -8
- package/src/process-map/activity-accent.ts +25 -0
- package/src/process-map/index.ts +1 -0
- package/src/process-map/map-model.test.ts +16 -0
- package/src/process-map/map-model.ts +323 -1
- package/src/process-map/object-centric-map.test.tsx +132 -0
- package/src/process-map/process-activity-node.tsx +152 -14
- package/src/process-map/process-map-object-centric.stories.tsx +219 -0
- package/src/process-map/process-map.stories.tsx +64 -0
- package/src/process-map/process-map.tsx +240 -16
- package/src/process-map/process-transition-edge.test.tsx +47 -0
- package/src/process-map/process-transition-edge.tsx +134 -7
- package/src/process-map/use-process-layout.ts +30 -9
- package/src/process-replay/congestion-heat.tsx +107 -0
- package/src/process-replay/index.ts +14 -0
- package/src/process-replay/process-replay.stories.tsx +168 -0
- package/src/process-replay/process-replay.test.tsx +170 -0
- package/src/process-replay/process-replay.tsx +285 -0
- package/src/process-replay/replay-controls.tsx +147 -0
- package/src/process-replay/replay-format.ts +83 -0
- package/src/process-replay/replay-tokens-context.ts +30 -0
- package/src/process-replay/use-controllable-value.ts +30 -0
- package/src/templates-process-explorer.stories.tsx +1304 -0
- package/src/test/contract.test.ts +66 -0
- package/src/test/contract.ts +107 -6
- package/src/test/doubles.test.tsx +87 -1
- package/src/test/doubles.tsx +174 -3
- package/src/test/index.ts +25 -1
- package/src/use-process-explorer/use-process-explorer.test.ts +44 -0
- package/src/use-process-explorer/use-process-explorer.ts +34 -2
- package/src/variant-explorer/coverage-bar.tsx +36 -0
- package/src/variant-explorer/index.ts +16 -0
- package/src/variant-explorer/sequence-chips.tsx +103 -0
- package/src/variant-explorer/variant-explorer-model.ts +42 -0
- package/src/variant-explorer/variant-explorer.stories.tsx +226 -0
- package/src/variant-explorer/variant-explorer.test.tsx +302 -0
- package/src/variant-explorer/variant-explorer.tsx +567 -0
- package/src/variant-explorer/variant-row.tsx +137 -0
- package/src/violation-list/index.ts +2 -0
- package/src/violation-list/violation-list.stories.tsx +73 -0
- package/src/violation-list/violation-list.test.tsx +84 -0
- package/src/violation-list/violation-list.tsx +259 -0
|
@@ -57,6 +57,25 @@ describe("assertProcessContract", () => {
|
|
|
57
57
|
).toThrow(ProcessContractError);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// DottedChart — RM-059
|
|
61
|
+
it("passes for a non-empty log with parsable timestamps, and throws otherwise", () => {
|
|
62
|
+
const spec: ProcessContractSpec = { dataProp: "log" };
|
|
63
|
+
const row = { caseId: "c1", activity: "A", timestamp: "2026-01-05T09:00:00Z" };
|
|
64
|
+
expect(() =>
|
|
65
|
+
assertProcessContract("DottedChartDouble", { log: { events: [row] } }, spec),
|
|
66
|
+
).not.toThrow();
|
|
67
|
+
expect(() => assertProcessContract("DottedChartDouble", { log: { events: [] } }, spec)).toThrow(
|
|
68
|
+
ProcessContractError,
|
|
69
|
+
);
|
|
70
|
+
expect(() =>
|
|
71
|
+
assertProcessContract(
|
|
72
|
+
"DottedChartDouble",
|
|
73
|
+
{ log: { events: [{ ...row, timestamp: "nope" }] } },
|
|
74
|
+
spec,
|
|
75
|
+
),
|
|
76
|
+
).toThrow(/parsable timestamps/);
|
|
77
|
+
});
|
|
78
|
+
|
|
60
79
|
it("throws when a required prop is undefined", () => {
|
|
61
80
|
const spec: ProcessContractSpec = { dataProp: "graph", requiredProps: ["onSelectionChange"] };
|
|
62
81
|
expect(() => assertProcessContract("ProcessMapDouble", { graph: emptyGraph }, spec)).toThrow(
|
|
@@ -65,6 +84,53 @@ describe("assertProcessContract", () => {
|
|
|
65
84
|
});
|
|
66
85
|
});
|
|
67
86
|
|
|
87
|
+
// PerformanceSpectrum — RM-060
|
|
88
|
+
describe("assertProcessContract — log + segment order", () => {
|
|
89
|
+
const LOG_SPEC: ProcessContractSpec = { dataProp: "log", segmentOrder: true };
|
|
90
|
+
const log = {
|
|
91
|
+
events: [
|
|
92
|
+
{ caseId: "c1", activity: "A", timestamp: 0 },
|
|
93
|
+
{ caseId: "c1", activity: "B", timestamp: 10 },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
it("throws when the log prop is not an EventLog", () => {
|
|
98
|
+
expect(() => assertProcessContract("PerformanceSpectrumDouble", {}, LOG_SPEC)).toThrow(
|
|
99
|
+
/"log" prop must be an EventLog/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("passes when the order resolves to a segment present in the log", () => {
|
|
104
|
+
expect(() =>
|
|
105
|
+
assertProcessContract("PerformanceSpectrumDouble", { log }, LOG_SPEC),
|
|
106
|
+
).not.toThrow();
|
|
107
|
+
expect(() =>
|
|
108
|
+
assertProcessContract(
|
|
109
|
+
"PerformanceSpectrumDouble",
|
|
110
|
+
{ log, order: [{ from: "A", to: "B" }] },
|
|
111
|
+
LOG_SPEC,
|
|
112
|
+
),
|
|
113
|
+
).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("throws when the order resolves to nothing in the log", () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
assertProcessContract(
|
|
119
|
+
"PerformanceSpectrumDouble",
|
|
120
|
+
{ log, order: [{ from: "B", to: "A" }] },
|
|
121
|
+
LOG_SPEC,
|
|
122
|
+
),
|
|
123
|
+
).toThrow(/resolves to no segment/);
|
|
124
|
+
expect(() =>
|
|
125
|
+
assertProcessContract(
|
|
126
|
+
"PerformanceSpectrumDouble",
|
|
127
|
+
{ log, order: { variantId: "x" } },
|
|
128
|
+
LOG_SPEC,
|
|
129
|
+
),
|
|
130
|
+
).toThrow(ProcessContractError);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
68
134
|
describe("buildProcessDoublePayload / readProcessDoubleProps round-trip", () => {
|
|
69
135
|
it("carries the graph's activity count and the current selection", () => {
|
|
70
136
|
const graph: ProcessGraph = { ...emptyGraph, activities: [...emptyGraph.activities] };
|
package/src/test/contract.ts
CHANGED
|
@@ -18,17 +18,37 @@
|
|
|
18
18
|
* `vi.mock("@elabs-ai/components-process", () => import("@elabs-ai/components-process/test"))`
|
|
19
19
|
* the way `@elabs-ai/components-charts` consumers do — that rename is out of this item's scope.
|
|
20
20
|
*/
|
|
21
|
-
import
|
|
21
|
+
import { toEpochMs } from "../core/event-log";
|
|
22
|
+
import { discoverGraph } from "../core/discover-graph";
|
|
23
|
+
import { extractVariants } from "../core/extract-variants";
|
|
24
|
+
import {
|
|
25
|
+
segmentOrderByFrequency,
|
|
26
|
+
segmentOrderForVariant,
|
|
27
|
+
segmentsFor,
|
|
28
|
+
type SegmentDefinition,
|
|
29
|
+
} from "../core/segments";
|
|
30
|
+
import type { ConformanceResult } from "../core/conformance";
|
|
31
|
+
import type { HappyPath } from "../core/reference-model";
|
|
32
|
+
import type { EventLog, ProcessGraph, Variant } from "../core/types";
|
|
22
33
|
|
|
23
34
|
/** Selection carried by a process view's coordinated-selection contract (RM-068 completes it). */
|
|
24
35
|
export type ProcessSelection = null | { kind: "node"; id: string } | { kind: "edge"; id: string };
|
|
25
36
|
|
|
26
37
|
/** What {@link assertProcessContract} checks for one double. */
|
|
27
38
|
export interface ProcessContractSpec {
|
|
28
|
-
/**
|
|
29
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Name of the prop carrying the double's primary data payload. `conformance` is a
|
|
41
|
+
* `ConformanceResult` and `value` a `HappyPath` (RM-062).
|
|
42
|
+
*/
|
|
43
|
+
dataProp: "graph" | "variants" | "log" | "conformance" | "value";
|
|
30
44
|
/** Other props the real component requires; the double must not silently accept `undefined`. */
|
|
31
45
|
requiredProps?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* PerformanceSpectrum — RM-060: the `order` prop (default `"frequency"`) must resolve to
|
|
48
|
+
* at least one segment that actually occurs in `log`, or the real view renders only its
|
|
49
|
+
* empty panel — almost always a test wired to the wrong activity names.
|
|
50
|
+
*/
|
|
51
|
+
segmentOrder?: boolean;
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
/** Thrown by {@link assertProcessContract} when a double is used with an invalid prop shape. */
|
|
@@ -48,6 +68,26 @@ function isProcessGraph(value: unknown): value is ProcessGraph {
|
|
|
48
68
|
);
|
|
49
69
|
}
|
|
50
70
|
|
|
71
|
+
function isEventLog(value: unknown): value is EventLog {
|
|
72
|
+
return !!value && typeof value === "object" && Array.isArray((value as EventLog).events);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** How many occurrences `order` (as `PerformanceSpectrum` reads it) finds in `log`. */
|
|
76
|
+
function countSpectrumOccurrences(log: EventLog, order: unknown, limit: unknown): number {
|
|
77
|
+
const cap = typeof limit === "number" ? Math.max(0, Math.floor(limit)) : 12;
|
|
78
|
+
let definitions: SegmentDefinition[] = [];
|
|
79
|
+
if (order === undefined || order === "frequency") {
|
|
80
|
+
definitions = segmentOrderByFrequency(discoverGraph(log), cap);
|
|
81
|
+
} else if (Array.isArray(order)) {
|
|
82
|
+
definitions = (order as SegmentDefinition[]).slice(0, cap);
|
|
83
|
+
} else if (order && typeof order === "object" && "variantId" in order) {
|
|
84
|
+
const id = (order as { variantId: unknown }).variantId;
|
|
85
|
+
const variant = extractVariants(log).find((v) => v.id === id);
|
|
86
|
+
definitions = variant ? segmentOrderForVariant(variant).slice(0, cap) : [];
|
|
87
|
+
}
|
|
88
|
+
return segmentsFor(log, definitions).length;
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
function isVariantArray(value: unknown): value is Variant[] {
|
|
52
92
|
return (
|
|
53
93
|
Array.isArray(value) &&
|
|
@@ -55,6 +95,32 @@ function isVariantArray(value: unknown): value is Variant[] {
|
|
|
55
95
|
);
|
|
56
96
|
}
|
|
57
97
|
|
|
98
|
+
// DottedChart — RM-059
|
|
99
|
+
/** A non-empty event log whose every row carries a parsable timestamp. */
|
|
100
|
+
function isTimedEventLog(value: unknown): value is EventLog {
|
|
101
|
+
const events = (value as EventLog | null | undefined)?.events;
|
|
102
|
+
return (
|
|
103
|
+
Array.isArray(events) &&
|
|
104
|
+
events.length > 0 &&
|
|
105
|
+
events.every(
|
|
106
|
+
(row) => !!row && typeof row === "object" && Number.isFinite(toEpochMs(row.timestamp)),
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isConformanceResult(value: unknown): value is ConformanceResult {
|
|
112
|
+
return (
|
|
113
|
+
!!value &&
|
|
114
|
+
typeof value === "object" &&
|
|
115
|
+
Array.isArray((value as ConformanceResult).traces) &&
|
|
116
|
+
typeof (value as ConformanceResult).deviationCounts === "object"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isHappyPath(value: unknown): value is HappyPath {
|
|
121
|
+
return !!value && typeof value === "object" && Array.isArray((value as HappyPath).steps);
|
|
122
|
+
}
|
|
123
|
+
|
|
58
124
|
/**
|
|
59
125
|
* Validate a double's props against its contract spec. Throws {@link ProcessContractError} on
|
|
60
126
|
* a missing/invalid required prop — mirroring what the real component would fail on at
|
|
@@ -79,6 +145,35 @@ export function assertProcessContract(
|
|
|
79
145
|
`"variants" prop must be a Variant[], got ${typeof data}`,
|
|
80
146
|
);
|
|
81
147
|
}
|
|
148
|
+
if (spec.dataProp === "log" && !isTimedEventLog(data)) {
|
|
149
|
+
throw new ProcessContractError(
|
|
150
|
+
componentName,
|
|
151
|
+
`"log" prop must be an EventLog with non-empty events and parsable timestamps`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
spec.segmentOrder &&
|
|
156
|
+
isEventLog(data) &&
|
|
157
|
+
data.events.length > 0 &&
|
|
158
|
+
countSpectrumOccurrences(data, props.order, props.segmentLimit) === 0
|
|
159
|
+
) {
|
|
160
|
+
throw new ProcessContractError(
|
|
161
|
+
componentName,
|
|
162
|
+
`"order" resolves to no segment that occurs in "log"`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (spec.dataProp === "conformance" && !isConformanceResult(data)) {
|
|
166
|
+
throw new ProcessContractError(
|
|
167
|
+
componentName,
|
|
168
|
+
`"conformance" prop must be a ConformanceResult, got ${typeof data}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (spec.dataProp === "value" && !isHappyPath(data)) {
|
|
172
|
+
throw new ProcessContractError(
|
|
173
|
+
componentName,
|
|
174
|
+
`"value" prop must be a HappyPath, got ${typeof data}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
82
177
|
for (const key of spec.requiredProps ?? []) {
|
|
83
178
|
if (props[key] === undefined) {
|
|
84
179
|
throw new ProcessContractError(componentName, `missing required prop "${key}"`);
|
|
@@ -103,9 +198,15 @@ export function buildProcessDoublePayload(
|
|
|
103
198
|
const dataLength =
|
|
104
199
|
spec.dataProp === "graph" && isProcessGraph(data)
|
|
105
200
|
? data.activities.length
|
|
106
|
-
:
|
|
107
|
-
? data.length
|
|
108
|
-
:
|
|
201
|
+
: spec.dataProp === "log" && isTimedEventLog(data)
|
|
202
|
+
? data.events.length
|
|
203
|
+
: spec.dataProp === "conformance" && isConformanceResult(data)
|
|
204
|
+
? data.traces.length
|
|
205
|
+
: spec.dataProp === "value" && isHappyPath(data)
|
|
206
|
+
? data.steps.length
|
|
207
|
+
: Array.isArray(data)
|
|
208
|
+
? data.length
|
|
209
|
+
: 0;
|
|
109
210
|
const payload: ProcessDoublePayload = { component: componentName, dataLength };
|
|
110
211
|
if ("selection" in props) payload.selection = props.selection as ProcessSelection;
|
|
111
212
|
return payload;
|
|
@@ -4,7 +4,19 @@ import { describe, expect, it } from "vitest";
|
|
|
4
4
|
import { discoverGraph } from "../core/discover-graph";
|
|
5
5
|
import { extractVariants } from "../core/extract-variants";
|
|
6
6
|
import { generateSyntheticLog } from "../core/fixtures/synthetic-log";
|
|
7
|
-
import {
|
|
7
|
+
import { liftHappyPath } from "../core/reference-model";
|
|
8
|
+
import { tokenReplay } from "../core/token-replay";
|
|
9
|
+
import {
|
|
10
|
+
ConformanceOverlayDouble,
|
|
11
|
+
DottedChartDouble,
|
|
12
|
+
HappyPathEditorDouble,
|
|
13
|
+
PerformanceSpectrumDouble,
|
|
14
|
+
ProcessKpiStripDouble,
|
|
15
|
+
ProcessMapDouble,
|
|
16
|
+
ProcessReplayDouble,
|
|
17
|
+
VariantExplorerDouble,
|
|
18
|
+
ViolationListDouble,
|
|
19
|
+
} from "./doubles";
|
|
8
20
|
import { readProcessDoubleProps } from "./contract";
|
|
9
21
|
|
|
10
22
|
const log = generateSyntheticLog({ cases: 20, seed: 7 });
|
|
@@ -44,8 +56,82 @@ describe("process test doubles", () => {
|
|
|
44
56
|
expect(node).toBeInstanceOf(HTMLDivElement);
|
|
45
57
|
});
|
|
46
58
|
|
|
59
|
+
// DottedChart — RM-059
|
|
60
|
+
it("DottedChartDouble mounts and records the event count", () => {
|
|
61
|
+
const { container } = render(<DottedChartDouble log={log} />);
|
|
62
|
+
const el = container.querySelector('[data-process-double="DottedChartDouble"]');
|
|
63
|
+
expect(readProcessDoubleProps(el as Element)?.dataLength).toBe(log.events.length);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("DottedChartDouble rejects an empty log or an unparsable timestamp", () => {
|
|
67
|
+
expect(() => render(<DottedChartDouble log={{ events: [] }} />)).toThrow(/DottedChartDouble/);
|
|
68
|
+
expect(() =>
|
|
69
|
+
render(
|
|
70
|
+
<DottedChartDouble log={{ events: [{ caseId: "c", activity: "A", timestamp: "nope" }] }} />,
|
|
71
|
+
),
|
|
72
|
+
).toThrow(/parsable timestamps/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// PerformanceSpectrum — RM-060
|
|
76
|
+
it("PerformanceSpectrumDouble records the event count and accepts the default order", () => {
|
|
77
|
+
const { container } = render(<PerformanceSpectrumDouble log={log} />);
|
|
78
|
+
const el = container.querySelector('[data-process-double="PerformanceSpectrumDouble"]');
|
|
79
|
+
expect(readProcessDoubleProps(el as Element)?.dataLength).toBe(log.events.length);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("PerformanceSpectrumDouble accepts a variant path and rejects an order absent from the log", () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
render(<PerformanceSpectrumDouble log={log} order={{ variantId: variants[0]!.id }} />),
|
|
85
|
+
).not.toThrow();
|
|
86
|
+
expect(() =>
|
|
87
|
+
render(<PerformanceSpectrumDouble log={log} order={[{ from: "Nope", to: "Never" }]} />),
|
|
88
|
+
).toThrow(/resolves to no segment/);
|
|
89
|
+
});
|
|
90
|
+
|
|
47
91
|
it("throws a contract error when required data is missing (a broken test fails loudly)", () => {
|
|
48
92
|
// @ts-expect-error -- deliberately omitting the required `graph` prop
|
|
49
93
|
expect(() => render(<ProcessMapDouble />)).toThrow(/ProcessMapDouble/);
|
|
50
94
|
});
|
|
51
95
|
});
|
|
96
|
+
|
|
97
|
+
describe("RM-062 doubles", () => {
|
|
98
|
+
const path = { id: "p", label: "Path", steps: [{ activity: "A" }, { activity: "B" }] };
|
|
99
|
+
const conformance = tokenReplay(log, liftHappyPath(path));
|
|
100
|
+
|
|
101
|
+
it("ConformanceOverlayDouble records the replayed case count and needs a graph", () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<ConformanceOverlayDouble graph={graph} conformance={conformance} />,
|
|
104
|
+
);
|
|
105
|
+
const el = container.querySelector('[data-process-double="ConformanceOverlayDouble"]');
|
|
106
|
+
expect(readProcessDoubleProps(el as Element)?.dataLength).toBe(conformance.traces.length);
|
|
107
|
+
// @ts-expect-error -- deliberately omitting the required `graph` prop
|
|
108
|
+
expect(() => render(<ConformanceOverlayDouble conformance={conformance} />)).toThrow(
|
|
109
|
+
/missing required prop "graph"/,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("ViolationListDouble rejects a non-ConformanceResult", () => {
|
|
114
|
+
// @ts-expect-error -- deliberately passing the wrong shape
|
|
115
|
+
expect(() => render(<ViolationListDouble conformance={graph} />)).toThrow(/ConformanceResult/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("HappyPathEditorDouble records the step count and requires onChange", () => {
|
|
119
|
+
const { container } = render(<HappyPathEditorDouble value={path} onChange={() => {}} />);
|
|
120
|
+
const el = container.querySelector('[data-process-double="HappyPathEditorDouble"]');
|
|
121
|
+
expect(readProcessDoubleProps(el as Element)?.dataLength).toBe(2);
|
|
122
|
+
// @ts-expect-error -- deliberately omitting the required `onChange` prop
|
|
123
|
+
expect(() => render(<HappyPathEditorDouble value={path} />)).toThrow(/onChange/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("RM-065 doubles", () => {
|
|
128
|
+
it("ProcessReplayDouble records the event count and needs a graph", () => {
|
|
129
|
+
const { container } = render(<ProcessReplayDouble graph={graph} log={log} />);
|
|
130
|
+
const el = container.querySelector('[data-process-double="ProcessReplayDouble"]');
|
|
131
|
+
expect(readProcessDoubleProps(el as Element)?.dataLength).toBe(log.events.length);
|
|
132
|
+
// @ts-expect-error -- deliberately omitting the required `graph` prop
|
|
133
|
+
expect(() => render(<ProcessReplayDouble log={log} />)).toThrow(
|
|
134
|
+
/missing required prop "graph"/,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/test/doubles.tsx
CHANGED
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
import { forwardRef } from "react";
|
|
13
13
|
import type { HTMLAttributes } from "react";
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { ConformanceResult } from "../core/conformance";
|
|
16
|
+
import type { HappyPath } from "../core/reference-model";
|
|
17
|
+
import type { EventLog, ProcessGraph, Variant } from "../core/types";
|
|
16
18
|
import {
|
|
17
19
|
assertProcessContract,
|
|
18
20
|
buildProcessDoublePayload,
|
|
21
|
+
ProcessContractError,
|
|
19
22
|
type ProcessContractSpec,
|
|
20
23
|
type ProcessSelection,
|
|
21
24
|
} from "./contract";
|
|
@@ -41,7 +44,10 @@ const PROCESS_MAP_SPEC: ProcessContractSpec = { dataProp: "graph" };
|
|
|
41
44
|
const VARIANT_EXPLORER_SPEC: ProcessContractSpec = { dataProp: "variants" };
|
|
42
45
|
const PROCESS_KPI_STRIP_SPEC: ProcessContractSpec = { dataProp: "graph" };
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
/** The only props the factory itself reads — a double's own contract is its `P`. */
|
|
48
|
+
type DoubleRenderProps = Pick<HTMLAttributes<HTMLDivElement>, "className" | "style">;
|
|
49
|
+
|
|
50
|
+
function createProcessDouble<P extends DoubleRenderProps>(name: string, spec: ProcessContractSpec) {
|
|
45
51
|
const Double = forwardRef<HTMLDivElement, P>(function ProcessTestDouble(props, ref) {
|
|
46
52
|
const record = props as unknown as Record<string, unknown>;
|
|
47
53
|
assertProcessContract(name, record, spec);
|
|
@@ -79,4 +85,169 @@ export const ProcessKpiStripDouble = createProcessDouble<ProcessKpiStripDoublePr
|
|
|
79
85
|
PROCESS_KPI_STRIP_SPEC,
|
|
80
86
|
);
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
// DottedChart — RM-059
|
|
89
|
+
interface DottedChartDoubleProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
90
|
+
log: EventLog;
|
|
91
|
+
selectedCaseIds?: readonly string[];
|
|
92
|
+
onSelect?: (caseIds: string[]) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Stand-in for `DottedChart` (RM-059). Asserts a non-empty `log.events` with parsable timestamps. */
|
|
96
|
+
export const DottedChartDouble = createProcessDouble<DottedChartDoubleProps>("DottedChartDouble", {
|
|
97
|
+
dataProp: "log",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// PerformanceSpectrum — RM-060
|
|
101
|
+
interface PerformanceSpectrumDoubleProps extends HTMLAttributes<HTMLDivElement> {
|
|
102
|
+
log: EventLog;
|
|
103
|
+
/** Explicit segments, `"frequency"` (the default) or `{ variantId }` — as the real prop. */
|
|
104
|
+
order?: Array<{ from: string; to: string; label?: string }> | "frequency" | { variantId: string };
|
|
105
|
+
segmentLimit?: number;
|
|
106
|
+
mode?: "lines" | "aggregated";
|
|
107
|
+
binSize?: number;
|
|
108
|
+
onFilterIntent?: (intent: { kind: "cases"; ids: string[] }) => void;
|
|
109
|
+
height?: number;
|
|
110
|
+
tableView?: boolean;
|
|
111
|
+
loading?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const PERFORMANCE_SPECTRUM_SPEC: ProcessContractSpec = { dataProp: "log", segmentOrder: true };
|
|
115
|
+
|
|
116
|
+
/** Stand-in for `PerformanceSpectrum` (RM-060); asserts `order` resolves to a segment present in `log`. */
|
|
117
|
+
export const PerformanceSpectrumDouble = createProcessDouble<PerformanceSpectrumDoubleProps>(
|
|
118
|
+
"PerformanceSpectrumDouble",
|
|
119
|
+
PERFORMANCE_SPECTRUM_SPEC,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// ── RM-062 ───────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
interface ConformanceOverlayDoubleProps extends DoubleOwnProps {
|
|
125
|
+
graph: ProcessGraph;
|
|
126
|
+
conformance: ConformanceResult;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface ViolationListDoubleProps extends HTMLAttributes<HTMLDivElement> {
|
|
130
|
+
conformance: ConformanceResult;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface HappyPathEditorDoubleProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
134
|
+
value: HappyPath;
|
|
135
|
+
onChange: (path: HappyPath) => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Stand-in for `ConformanceOverlay` (RM-062): requires a graph AND a replay result. */
|
|
139
|
+
export const ConformanceOverlayDouble = createProcessDouble<ConformanceOverlayDoubleProps>(
|
|
140
|
+
"ConformanceOverlayDouble",
|
|
141
|
+
{ dataProp: "conformance", requiredProps: ["graph"] },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/** Stand-in for `ViolationList` (RM-062). */
|
|
145
|
+
export const ViolationListDouble = createProcessDouble<ViolationListDoubleProps>(
|
|
146
|
+
"ViolationListDouble",
|
|
147
|
+
{ dataProp: "conformance" },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
/** Stand-in for `HappyPathEditor` (RM-062): a controlled editor, so `onChange` is required. */
|
|
151
|
+
export const HappyPathEditorDouble = createProcessDouble<HappyPathEditorDoubleProps>(
|
|
152
|
+
"HappyPathEditorDouble",
|
|
153
|
+
{ dataProp: "value", requiredProps: ["onChange"] },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// ── RM-065 ───────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
interface ProcessReplayDoubleProps extends HTMLAttributes<HTMLDivElement> {
|
|
159
|
+
graph: ProcessGraph;
|
|
160
|
+
log: EventLog;
|
|
161
|
+
synchronizedStart?: boolean;
|
|
162
|
+
bucketMs?: number;
|
|
163
|
+
playing?: boolean;
|
|
164
|
+
defaultPlaying?: boolean;
|
|
165
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
166
|
+
time?: number;
|
|
167
|
+
defaultTime?: number;
|
|
168
|
+
onTimeChange?: (time: number) => void;
|
|
169
|
+
speed?: number;
|
|
170
|
+
defaultSpeed?: number;
|
|
171
|
+
onSpeedChange?: (speed: number) => void;
|
|
172
|
+
congestionLimit?: number;
|
|
173
|
+
loading?: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Stand-in for `ProcessReplay` (RM-065): requires the log (per-case timing) AND the graph. */
|
|
177
|
+
export const ProcessReplayDouble = createProcessDouble<ProcessReplayDoubleProps>(
|
|
178
|
+
"ProcessReplayDouble",
|
|
179
|
+
{ dataProp: "log", requiredProps: ["graph"] },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
export type {
|
|
183
|
+
ProcessReplayDoubleProps,
|
|
184
|
+
DottedChartDoubleProps,
|
|
185
|
+
PerformanceSpectrumDoubleProps,
|
|
186
|
+
ProcessMapDoubleProps,
|
|
187
|
+
VariantExplorerDoubleProps,
|
|
188
|
+
ProcessKpiStripDoubleProps,
|
|
189
|
+
ConformanceOverlayDoubleProps,
|
|
190
|
+
ViolationListDoubleProps,
|
|
191
|
+
HappyPathEditorDoubleProps,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── ProcessCompareDouble — RM-064 ────────────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// `ProcessCompare` composes `ProcessMap` twice (or once, superimposed) — a REAL component
|
|
197
|
+
// this package already ships, unlike the pre-launch stand-ins above. Its own contract does
|
|
198
|
+
// not fit `ProcessContractSpec` (`dataProp: "graph" | "variants"` — a single top-level
|
|
199
|
+
// payload), because `ProcessCompare`'s payload is a PAIR (`a`/`b`), so this double validates
|
|
200
|
+
// and serializes its own shape directly rather than stretching the shared engine.
|
|
201
|
+
|
|
202
|
+
interface ProcessCompareSideDoubleProps {
|
|
203
|
+
label: string;
|
|
204
|
+
graph?: ProcessGraph;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface ProcessCompareDoubleProps extends HTMLAttributes<HTMLDivElement> {
|
|
208
|
+
a: ProcessCompareSideDoubleProps;
|
|
209
|
+
b: ProcessCompareSideDoubleProps;
|
|
210
|
+
mode?: "side-by-side" | "superimposed";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** What `ProcessCompareDouble` records to `data-process-props`. */
|
|
214
|
+
interface ProcessCompareDoublePayload {
|
|
215
|
+
component: "ProcessCompareDouble";
|
|
216
|
+
mode: "side-by-side" | "superimposed";
|
|
217
|
+
aLabel: string;
|
|
218
|
+
bLabel: string;
|
|
219
|
+
aActivityCount: number;
|
|
220
|
+
bActivityCount: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Stand-in for `ProcessCompare` (RM-064) — cheap enough to mount without a real canvas. */
|
|
224
|
+
export const ProcessCompareDouble = forwardRef<HTMLDivElement, ProcessCompareDoubleProps>(
|
|
225
|
+
function ProcessCompareDouble({ a, b, mode = "side-by-side", className, style }, ref) {
|
|
226
|
+
if (!a || typeof a.label !== "string") {
|
|
227
|
+
throw new ProcessContractError("ProcessCompareDouble", 'missing required prop "a.label"');
|
|
228
|
+
}
|
|
229
|
+
if (!b || typeof b.label !== "string") {
|
|
230
|
+
throw new ProcessContractError("ProcessCompareDouble", 'missing required prop "b.label"');
|
|
231
|
+
}
|
|
232
|
+
const payload: ProcessCompareDoublePayload = {
|
|
233
|
+
component: "ProcessCompareDouble",
|
|
234
|
+
mode,
|
|
235
|
+
aLabel: a.label,
|
|
236
|
+
bLabel: b.label,
|
|
237
|
+
aActivityCount: a.graph?.activities.length ?? 0,
|
|
238
|
+
bActivityCount: b.graph?.activities.length ?? 0,
|
|
239
|
+
};
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
ref={ref}
|
|
243
|
+
data-slot="process-test-double"
|
|
244
|
+
data-process-double="ProcessCompareDouble"
|
|
245
|
+
data-process-props={JSON.stringify(payload)}
|
|
246
|
+
className={className}
|
|
247
|
+
style={style}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
export type { ProcessCompareDoubleProps, ProcessCompareDoublePayload };
|
package/src/test/index.ts
CHANGED
|
@@ -22,13 +22,37 @@ export {
|
|
|
22
22
|
export type { ProcessContractSpec, ProcessDoublePayload, ProcessSelection } from "./contract";
|
|
23
23
|
|
|
24
24
|
// Doubles
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
PerformanceSpectrumDouble,
|
|
27
|
+
ProcessCompareDouble,
|
|
28
|
+
ProcessKpiStripDouble,
|
|
29
|
+
ProcessMapDouble,
|
|
30
|
+
VariantExplorerDouble,
|
|
31
|
+
} from "./doubles";
|
|
26
32
|
export type {
|
|
33
|
+
PerformanceSpectrumDoubleProps,
|
|
34
|
+
ProcessCompareDoubleProps,
|
|
27
35
|
ProcessKpiStripDoubleProps,
|
|
28
36
|
ProcessMapDoubleProps,
|
|
29
37
|
VariantExplorerDoubleProps,
|
|
30
38
|
} from "./doubles";
|
|
31
39
|
|
|
40
|
+
// DottedChart — RM-059
|
|
41
|
+
export { DottedChartDouble } from "./doubles";
|
|
42
|
+
export type { DottedChartDoubleProps } from "./doubles";
|
|
43
|
+
|
|
44
|
+
// Doubles — RM-062
|
|
45
|
+
export { ConformanceOverlayDouble, HappyPathEditorDouble, ViolationListDouble } from "./doubles";
|
|
46
|
+
export type {
|
|
47
|
+
ConformanceOverlayDoubleProps,
|
|
48
|
+
HappyPathEditorDoubleProps,
|
|
49
|
+
ViolationListDoubleProps,
|
|
50
|
+
} from "./doubles";
|
|
51
|
+
|
|
52
|
+
// Doubles — RM-065
|
|
53
|
+
export { ProcessReplayDouble } from "./doubles";
|
|
54
|
+
export type { ProcessReplayDoubleProps } from "./doubles";
|
|
55
|
+
|
|
32
56
|
// Fixture helper
|
|
33
57
|
export { withProcessFixture } from "./primitives";
|
|
34
58
|
export type { ProcessFixture } from "./primitives";
|
|
@@ -264,6 +264,50 @@ describe("useProcessExplorer — filter intents (the RM-052 acceptance criterion
|
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
describe("useProcessExplorer — excludedByIntent (RM-056, #205)", () => {
|
|
268
|
+
it("is empty with no active intents", () => {
|
|
269
|
+
const { result } = renderHook(() => useProcessExplorer(orderToCash));
|
|
270
|
+
expect(result.current.excludedByIntent).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("hand-computed against the fixture: a three-intent chain, each entry the MARGINAL exclusion", () => {
|
|
274
|
+
// Fixture cases: case-1/2 (happy path), case-3 (Reject Order), case-4 (Amend Order,
|
|
275
|
+
// then the happy path), case-5 (happy path, Send Invoice before Ship).
|
|
276
|
+
const { result } = renderHook(() => useProcessExplorer(orderToCash));
|
|
277
|
+
|
|
278
|
+
// A: drop case-3 (the only case with "Reject Order") — 5 cases -> 4, excludes 1.
|
|
279
|
+
act(() => result.current.applyIntent({ kind: "without", activity: "Reject Order" }));
|
|
280
|
+
// B: of the remaining 4 (case-1/2/4/5), keep only "Amend Order" — just case-4 — so B
|
|
281
|
+
// alone (on top of A) excludes the other 3 (case-1/2/5).
|
|
282
|
+
act(() => result.current.applyIntent({ kind: "with", activity: "Amend Order" }));
|
|
283
|
+
// C: of the remaining 1 (case-4), "endsWith Receive Payment" still holds — excludes 0.
|
|
284
|
+
act(() => result.current.applyIntent({ kind: "endsWith", activity: "Receive Payment" }));
|
|
285
|
+
|
|
286
|
+
expect(result.current.intents).toEqual([
|
|
287
|
+
{ kind: "without", activity: "Reject Order" },
|
|
288
|
+
{ kind: "with", activity: "Amend Order" },
|
|
289
|
+
{ kind: "endsWith", activity: "Receive Payment" },
|
|
290
|
+
]);
|
|
291
|
+
expect(result.current.excludedByIntent).toEqual([1, 3, 0]);
|
|
292
|
+
// The chain's own final case count agrees with the KPI strip's own number — no
|
|
293
|
+
// double counting between the per-chip figures and the whole-chain total.
|
|
294
|
+
expect(result.current.kpis.cases).toBe(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("stays parallel to `intents` after clearIntent drops an entry from the middle", () => {
|
|
298
|
+
const { result } = renderHook(() => useProcessExplorer(orderToCash));
|
|
299
|
+
act(() => result.current.applyIntent({ kind: "without", activity: "Reject Order" }));
|
|
300
|
+
act(() => result.current.applyIntent({ kind: "with", activity: "Amend Order" }));
|
|
301
|
+
expect(result.current.excludedByIntent).toEqual([1, 3]);
|
|
302
|
+
|
|
303
|
+
act(() => result.current.clearIntent(0));
|
|
304
|
+
expect(result.current.intents).toEqual([{ kind: "with", activity: "Amend Order" }]);
|
|
305
|
+
// Recomputed from scratch against the NEW chain — "with Amend Order" alone, against
|
|
306
|
+
// the full 5-case log, excludes the 4 cases that never touch "Amend Order".
|
|
307
|
+
expect(result.current.excludedByIntent).toEqual([4]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
267
311
|
describe("useProcessExplorer — selectionStates.variants (RM-052 round 3, #227, G2)", () => {
|
|
268
312
|
it('marks each id of an active "variant" intent "selected"', () => {
|
|
269
313
|
const { result } = renderHook(() => useProcessExplorer(orderToCash));
|