@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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { activityColorScale } from "../core/activity-color-scale";
|
|
5
|
+
import { discoverGraph } from "../core/discover-graph";
|
|
6
|
+
import { extractVariants } from "../core/extract-variants";
|
|
7
|
+
import { generateSyntheticLog } from "../core/fixtures/synthetic-log";
|
|
8
|
+
import type { Variant } from "../core/types";
|
|
9
|
+
import { ProcessMap } from "../process-map/process-map";
|
|
10
|
+
import { VariantExplorer } from "./variant-explorer";
|
|
11
|
+
import { selectVariantsByCoverage } from "./variant-explorer-model";
|
|
12
|
+
|
|
13
|
+
// React Flow's transform math needs these under jsdom — see `process-map.test.tsx`.
|
|
14
|
+
if (typeof globalThis.DOMMatrixReadOnly === "undefined") {
|
|
15
|
+
class DOMMatrixReadOnlyPolyfill {
|
|
16
|
+
m22 = 1;
|
|
17
|
+
constructor(_init?: unknown) {}
|
|
18
|
+
}
|
|
19
|
+
globalThis.DOMMatrixReadOnly = DOMMatrixReadOnlyPolyfill as unknown as typeof DOMMatrixReadOnly;
|
|
20
|
+
}
|
|
21
|
+
if (typeof globalThis.DOMMatrix === "undefined") {
|
|
22
|
+
globalThis.DOMMatrix = globalThis.DOMMatrixReadOnly as unknown as typeof DOMMatrix;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// jsdom has no layout, so the virtualizer would see a 0px viewport. Give the explorer's
|
|
26
|
+
// viewport a real height; everything else keeps jsdom's zero box.
|
|
27
|
+
const VIEWPORT_HEIGHT = 400;
|
|
28
|
+
// `@tanstack/virtual-core` reads the scroll element's `offsetWidth`/`offsetHeight`.
|
|
29
|
+
const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
|
|
30
|
+
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");
|
|
31
|
+
const isViewport = (el: HTMLElement) => el.dataset?.slot === "variant-explorer-viewport";
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
34
|
+
configurable: true,
|
|
35
|
+
get(this: HTMLElement) {
|
|
36
|
+
return isViewport(this) ? VIEWPORT_HEIGHT : 0;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
|
|
40
|
+
configurable: true,
|
|
41
|
+
get(this: HTMLElement) {
|
|
42
|
+
return isViewport(this) ? 800 : 0;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
if (originalOffsetHeight)
|
|
48
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", originalOffsetHeight);
|
|
49
|
+
if (originalOffsetWidth)
|
|
50
|
+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", originalOffsetWidth);
|
|
51
|
+
});
|
|
52
|
+
afterEach(cleanup);
|
|
53
|
+
|
|
54
|
+
const log = generateSyntheticLog({ cases: 120, seed: 5 });
|
|
55
|
+
const graph = discoverGraph(log);
|
|
56
|
+
const variants = extractVariants(log);
|
|
57
|
+
const scale = activityColorScale(graph);
|
|
58
|
+
|
|
59
|
+
function manyVariants(count: number): Variant[] {
|
|
60
|
+
const base = variants[0]!;
|
|
61
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
62
|
+
...base,
|
|
63
|
+
id: `v-${i}`,
|
|
64
|
+
count: count - i,
|
|
65
|
+
share: 1 / count,
|
|
66
|
+
cumulativeShare: (i + 1) / count,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("selectVariantsByCoverage", () => {
|
|
71
|
+
it("selects the smallest prefix whose cumulative share reaches the target", () => {
|
|
72
|
+
const ids = selectVariantsByCoverage(variants, 0.5);
|
|
73
|
+
const lastIndex = ids.length - 1;
|
|
74
|
+
expect(variants[lastIndex]!.cumulativeShare).toBeGreaterThanOrEqual(0.5);
|
|
75
|
+
if (lastIndex > 0) expect(variants[lastIndex - 1]!.cumulativeShare).toBeLessThan(0.5);
|
|
76
|
+
expect(ids).toEqual(variants.slice(0, ids.length).map((v) => v.id));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("is deterministic — same target, same id list", () => {
|
|
80
|
+
expect(selectVariantsByCoverage(variants, 0.8)).toEqual(
|
|
81
|
+
selectVariantsByCoverage(variants, 0.8),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("selects nothing at 0 and everything at 1", () => {
|
|
86
|
+
expect(selectVariantsByCoverage(variants, 0)).toEqual([]);
|
|
87
|
+
expect(selectVariantsByCoverage(variants, 1)).toHaveLength(variants.length);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("VariantExplorer — rows", () => {
|
|
92
|
+
it("names each row with its rank, activity count, cases and coverage", () => {
|
|
93
|
+
render(<VariantExplorer variants={variants} colorScale={scale} onSelect={() => {}} />);
|
|
94
|
+
const first = variants[0]!;
|
|
95
|
+
const checkbox = screen.getAllByRole("checkbox")[0]!;
|
|
96
|
+
expect(checkbox).toHaveAccessibleName(
|
|
97
|
+
`Variant 1, ${first.sequence.length} activities, ${first.count} cases, ${Math.round(first.share * 100)} percent coverage`,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("prints activity labels on chips, and two-letter codes in the DNA strip", () => {
|
|
102
|
+
const { rerender } = render(
|
|
103
|
+
<VariantExplorer variants={variants} colorScale={scale} onSelect={() => {}} />,
|
|
104
|
+
);
|
|
105
|
+
const firstRow = document.querySelector('[data-slot="variant-explorer-row"]')!;
|
|
106
|
+
const firstActivity = variants[0]!.sequence[0]!;
|
|
107
|
+
const chip = firstRow.querySelector('[data-slot="variant-explorer-chip"]')!;
|
|
108
|
+
expect(chip).toHaveTextContent(scale.labelFor(firstActivity));
|
|
109
|
+
|
|
110
|
+
rerender(
|
|
111
|
+
<VariantExplorer variants={variants} colorScale={scale} onSelect={() => {}} abbreviate />,
|
|
112
|
+
);
|
|
113
|
+
const abbreviated = document
|
|
114
|
+
.querySelector('[data-slot="variant-explorer-row"]')!
|
|
115
|
+
.querySelector('[data-slot="variant-explorer-chip"]')!;
|
|
116
|
+
expect(abbreviated).toHaveTextContent(scale.codeFor(firstActivity));
|
|
117
|
+
// The strip's accessible name still carries full labels, never the codes.
|
|
118
|
+
expect(document.querySelector('[data-slot="variant-explorer-sequence"]')).toHaveAccessibleName(
|
|
119
|
+
expect.stringContaining(scale.labelFor(firstActivity)),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('sequenceDisplay="swatch" draws text-free identity blocks that still name their activity', () => {
|
|
124
|
+
render(
|
|
125
|
+
<VariantExplorer
|
|
126
|
+
variants={variants}
|
|
127
|
+
colorScale={scale}
|
|
128
|
+
onSelect={() => {}}
|
|
129
|
+
sequenceDisplay="swatch"
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
const firstActivity = variants[0]!.sequence[0]!;
|
|
133
|
+
const strip = document.querySelector('[data-slot="variant-explorer-sequence"]')!;
|
|
134
|
+
expect(strip).toHaveAttribute("data-display", "swatch");
|
|
135
|
+
const chip = strip.querySelector('[data-slot="variant-explorer-chip"]')!;
|
|
136
|
+
expect(chip).toHaveTextContent("");
|
|
137
|
+
expect(chip).toHaveAttribute("title", scale.labelFor(firstActivity));
|
|
138
|
+
expect(chip).toHaveAttribute("data-activity", firstActivity);
|
|
139
|
+
// Colour is not the only channel: the strip's accessible name lists the full sequence.
|
|
140
|
+
expect(strip).toHaveAccessibleName(expect.stringContaining(scale.labelFor(firstActivity)));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("reflects selectionStates.variants on the checkbox and the row", () => {
|
|
144
|
+
const id = variants[0]!.id;
|
|
145
|
+
render(
|
|
146
|
+
<VariantExplorer
|
|
147
|
+
variants={variants}
|
|
148
|
+
colorScale={scale}
|
|
149
|
+
selectionStates={{ variants: { [id]: "selected" } }}
|
|
150
|
+
onSelect={() => {}}
|
|
151
|
+
/>,
|
|
152
|
+
);
|
|
153
|
+
const row = document.querySelector(`[data-variant="${id}"]`)!;
|
|
154
|
+
expect(row).toHaveAttribute("data-selection", "selected");
|
|
155
|
+
expect(within(row as HTMLElement).getByRole("checkbox")).toBeChecked();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("shows a loading panel and a real empty state", () => {
|
|
159
|
+
const { rerender } = render(
|
|
160
|
+
<VariantExplorer variants={[]} colorScale={scale} onSelect={() => {}} loading />,
|
|
161
|
+
);
|
|
162
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
163
|
+
rerender(<VariantExplorer variants={[]} colorScale={scale} onSelect={() => {}} />);
|
|
164
|
+
expect(screen.getByText("No variants")).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders the table twin with one row per variant", () => {
|
|
168
|
+
render(
|
|
169
|
+
<VariantExplorer variants={variants} colorScale={scale} onSelect={() => {}} tableView />,
|
|
170
|
+
);
|
|
171
|
+
// header row + one per variant
|
|
172
|
+
expect(screen.getAllByRole("row")).toHaveLength(variants.length + 1);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("VariantExplorer — selection", () => {
|
|
177
|
+
it("toggles a variant on row click", async () => {
|
|
178
|
+
const onSelect = vi.fn();
|
|
179
|
+
render(<VariantExplorer variants={variants} colorScale={scale} onSelect={onSelect} />);
|
|
180
|
+
const row = document.querySelector('[data-slot="variant-explorer-row"]') as HTMLElement;
|
|
181
|
+
await userEvent.click(within(row).getByRole("img"));
|
|
182
|
+
expect(onSelect).toHaveBeenCalledWith([variants[0]!.id], "toggle");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("moves focus with arrow keys and toggles with Space and Enter", async () => {
|
|
186
|
+
const onSelect = vi.fn();
|
|
187
|
+
render(<VariantExplorer variants={variants} colorScale={scale} onSelect={onSelect} />);
|
|
188
|
+
const user = userEvent.setup();
|
|
189
|
+
await user.tab();
|
|
190
|
+
const first = screen.getAllByRole("checkbox")[0]!;
|
|
191
|
+
expect(first).toHaveFocus();
|
|
192
|
+
|
|
193
|
+
await user.keyboard("{ArrowDown}");
|
|
194
|
+
const second = document.querySelector('[data-index="1"] [data-slot="checkbox"]');
|
|
195
|
+
await waitFor(() => expect(second).toHaveFocus());
|
|
196
|
+
|
|
197
|
+
await user.keyboard(" ");
|
|
198
|
+
expect(onSelect).toHaveBeenLastCalledWith([variants[1]!.id], "toggle");
|
|
199
|
+
|
|
200
|
+
await user.keyboard("{Enter}");
|
|
201
|
+
expect(onSelect).toHaveBeenLastCalledWith([variants[1]!.id], "toggle");
|
|
202
|
+
expect(onSelect).toHaveBeenCalledTimes(2);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("emits a variant filter intent for the selected ids", async () => {
|
|
206
|
+
const onFilterIntent = vi.fn();
|
|
207
|
+
const ids = [variants[0]!.id, variants[2]!.id];
|
|
208
|
+
render(
|
|
209
|
+
<VariantExplorer
|
|
210
|
+
variants={variants}
|
|
211
|
+
colorScale={scale}
|
|
212
|
+
selectionStates={{ variants: Object.fromEntries(ids.map((id) => [id, "selected"])) }}
|
|
213
|
+
onSelect={() => {}}
|
|
214
|
+
onFilterIntent={onFilterIntent}
|
|
215
|
+
/>,
|
|
216
|
+
);
|
|
217
|
+
await userEvent.click(screen.getByRole("button", { name: "Show only selected variants" }));
|
|
218
|
+
expect(onFilterIntent).toHaveBeenCalledWith({ kind: "variant", ids });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("selects the coverage prefix with mode replace when the slider commits", async () => {
|
|
222
|
+
const onSelect = vi.fn();
|
|
223
|
+
render(
|
|
224
|
+
<VariantExplorer
|
|
225
|
+
variants={variants}
|
|
226
|
+
colorScale={scale}
|
|
227
|
+
onSelect={onSelect}
|
|
228
|
+
coverageTarget={0.5}
|
|
229
|
+
/>,
|
|
230
|
+
);
|
|
231
|
+
const thumb = screen.getByRole("slider", { name: "Coverage target" });
|
|
232
|
+
act(() => thumb.focus());
|
|
233
|
+
fireEvent.keyDown(thumb, { key: "ArrowRight" });
|
|
234
|
+
expect(onSelect).toHaveBeenLastCalledWith(selectVariantsByCoverage(variants, 0.51), "replace");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("VariantExplorer — virtualization (2 000 variants)", () => {
|
|
239
|
+
it("mounts a bounded window of rows, and still a bounded window after scrolling", async () => {
|
|
240
|
+
const big = manyVariants(2000);
|
|
241
|
+
render(<VariantExplorer variants={big} colorScale={scale} onSelect={() => {}} />);
|
|
242
|
+
const rowCount = () => document.querySelectorAll('[data-slot="variant-explorer-row"]').length;
|
|
243
|
+
const bound = Math.ceil(VIEWPORT_HEIGHT / 40) + 2 * 6 + 2;
|
|
244
|
+
await waitFor(() => expect(rowCount()).toBeGreaterThan(0));
|
|
245
|
+
expect(rowCount()).toBeLessThanOrEqual(bound);
|
|
246
|
+
|
|
247
|
+
const viewport = document.querySelector<HTMLElement>(
|
|
248
|
+
'[data-slot="variant-explorer-viewport"]',
|
|
249
|
+
)!;
|
|
250
|
+
act(() => {
|
|
251
|
+
viewport.scrollTop = 40 * 1500;
|
|
252
|
+
fireEvent.scroll(viewport);
|
|
253
|
+
});
|
|
254
|
+
await waitFor(() => expect(document.querySelector('[data-index="1500"]')).toBeInTheDocument());
|
|
255
|
+
expect(rowCount()).toBeLessThanOrEqual(bound);
|
|
256
|
+
expect(document.querySelector('[data-index="0"]')).not.toBeInTheDocument();
|
|
257
|
+
// The roving tab stop follows the window, so Tab can still reach the list.
|
|
258
|
+
expect(document.querySelectorAll('[data-slot="checkbox"][tabindex="0"]')).toHaveLength(1);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("VariantExplorer + ProcessMap — one colour per activity", () => {
|
|
263
|
+
it("paints every shared activity's chip exactly like its map node accent", async () => {
|
|
264
|
+
render(
|
|
265
|
+
<>
|
|
266
|
+
<ProcessMap
|
|
267
|
+
graph={graph}
|
|
268
|
+
metric={{ node: "absolute_case", edge: "absolute" }}
|
|
269
|
+
colorScale={scale}
|
|
270
|
+
/>
|
|
271
|
+
<VariantExplorer variants={variants} colorScale={scale} onSelect={() => {}} />
|
|
272
|
+
</>,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await waitFor(() =>
|
|
276
|
+
expect(document.querySelectorAll('[data-slot="process-activity-node-accent"]').length).toBe(
|
|
277
|
+
graph.activities.length,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const chips = document.querySelectorAll<HTMLElement>('[data-slot="variant-explorer-chip"]');
|
|
282
|
+
expect(chips.length).toBeGreaterThan(0);
|
|
283
|
+
const checked = new Set<string>();
|
|
284
|
+
for (const chip of chips) {
|
|
285
|
+
const activity = chip.dataset.activity!;
|
|
286
|
+
const node = document.querySelector(`.react-flow__node[data-id="${CSS.escape(activity)}"]`);
|
|
287
|
+
expect(node).toBeTruthy();
|
|
288
|
+
const accent = node!.querySelector<HTMLElement>(
|
|
289
|
+
'[data-slot="process-activity-node-accent"]',
|
|
290
|
+
)!;
|
|
291
|
+
const swatch = chip.querySelector<HTMLElement>('[data-slot="variant-explorer-chip-swatch"]')!;
|
|
292
|
+
expect(swatch.dataset.colorToken).toBe(accent.dataset.colorToken);
|
|
293
|
+
expect(swatch.dataset.colorToken).toBe(scale.colorFor(activity).token);
|
|
294
|
+
// Guard against a vacuous pass: jsdom must have kept the token-backed fill.
|
|
295
|
+
expect(swatch.style.backgroundColor).toContain(`var(${scale.colorFor(activity).token})`);
|
|
296
|
+
expect(swatch.style.backgroundColor).toBe(accent.style.backgroundColor);
|
|
297
|
+
expect(swatch.style.backgroundImage).toBe(accent.style.backgroundImage);
|
|
298
|
+
checked.add(activity);
|
|
299
|
+
}
|
|
300
|
+
expect(checked.size).toBeGreaterThan(1);
|
|
301
|
+
});
|
|
302
|
+
});
|