@handled-ai/design-system 0.17.1 → 0.18.1
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/dist/charts/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +29 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/actor-byline.d.ts +3 -0
- package/dist/components/actor-byline.js +5 -0
- package/dist/components/actor-byline.js.map +1 -0
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- package/dist/components/feedback-primitives.d.ts +66 -0
- package/dist/components/feedback-primitives.js +295 -0
- package/dist/components/feedback-primitives.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +86 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/performance-metrics-table.d.ts +2 -1
- package/dist/components/performance-metrics-table.js +78 -46
- package/dist/components/performance-metrics-table.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.js.map +1 -0
- package/dist/components/score-why-chips.d.ts +8 -17
- package/dist/components/score-why-chips.js +266 -180
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +17 -0
- package/dist/components/signal-priority-popover.js +247 -0
- package/dist/components/signal-priority-popover.js.map +1 -0
- package/dist/components/user-display.d.ts +22 -0
- package/dist/components/user-display.js +138 -0
- package/dist/components/user-display.js.map +1 -0
- package/dist/components/user-pill.d.ts +3 -0
- package/dist/components/user-pill.js +5 -0
- package/dist/components/user-pill.js.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/user-display.d.ts +31 -0
- package/dist/lib/user-display.js +57 -0
- package/dist/lib/user-display.js.map +1 -0
- package/dist/prototype/index.d.ts +2 -1
- package/dist/prototype/prototype-accounts-view.d.ts +2 -1
- package/dist/prototype/prototype-admin-view.d.ts +2 -1
- package/dist/prototype/prototype-config.d.ts +15 -332
- package/dist/prototype/prototype-inbox-view.d.ts +2 -1
- package/dist/prototype/prototype-inbox-view.js +11 -12
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +2 -1
- package/dist/prototype/prototype-shell.d.ts +2 -1
- package/dist/signal-priority-popover-DQ_VuHac.d.ts +390 -0
- package/package.json +1 -1
- package/src/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +38 -1
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +99 -188
- package/src/components/__tests__/feedback-primitives.test.tsx +509 -0
- package/src/components/__tests__/insights-primitives.test.tsx +117 -0
- package/src/components/__tests__/performance-metrics-table.test.tsx +54 -0
- package/src/components/__tests__/score-why-chips.test.tsx +540 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +312 -0
- package/src/components/__tests__/user-display.test.tsx +75 -0
- package/src/components/actor-byline.tsx +1 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/feedback-primitives.tsx +424 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +82 -0
- package/src/components/performance-metrics-table.tsx +99 -63
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -0
- package/src/components/score-why-chips.tsx +413 -203
- package/src/components/signal-priority-popover.tsx +359 -0
- package/src/components/user-display.tsx +96 -0
- package/src/components/user-pill.tsx +1 -0
- package/src/index.ts +11 -0
- package/src/lib/__tests__/user-display.test.ts +85 -0
- package/src/lib/user-display.ts +88 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +33 -29
- package/src/prototype/__tests__/detail-view-title-slots.test.tsx +65 -0
- package/src/prototype/prototype-config.ts +28 -4
- package/src/prototype/prototype-inbox-view.tsx +8 -10
- package/src/prototype/__tests__/detail-view-title-subtext.test.tsx +0 -72
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ScoreWhyChips, WhyPill, WhyCard, and signal row components.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - ScoreWhyChips renders bucket pills with structured signal rows
|
|
6
|
+
* - Slot grammar grid renders primary value, qualifier, counterparty, time
|
|
7
|
+
* - Missing data renders empty (no em-dashes)
|
|
8
|
+
* - Long list truncation at 8 rows, "Show N more" expands
|
|
9
|
+
* - Combined signal row renders component mini-chips
|
|
10
|
+
* - Bucket feedback footer renders and fires callback with FeedbackSubmitData
|
|
11
|
+
* - WhyPill shows icon, count badge, chevron, and X close button
|
|
12
|
+
* - Accordion behavior: only one pill expanded at a time
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, vi } from "vitest"
|
|
16
|
+
import React from "react"
|
|
17
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
18
|
+
import { ScoreWhyChips } from "../score-why-chips"
|
|
19
|
+
import type {
|
|
20
|
+
QueueItem,
|
|
21
|
+
SignalScoreData,
|
|
22
|
+
SignalScoreExplanationBucket,
|
|
23
|
+
SignalScoreExplanationSignal,
|
|
24
|
+
} from "../../prototype/prototype-config"
|
|
25
|
+
|
|
26
|
+
// ─── Mock data ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const mockItem: QueueItem = {
|
|
29
|
+
id: "case-1",
|
|
30
|
+
title: "Test Case",
|
|
31
|
+
details: "Some details",
|
|
32
|
+
statusColor: "red",
|
|
33
|
+
time: "2h ago",
|
|
34
|
+
company: "TestCorp",
|
|
35
|
+
tag1: "urgent",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeSignal(overrides: Partial<SignalScoreExplanationSignal> = {}): SignalScoreExplanationSignal {
|
|
39
|
+
return {
|
|
40
|
+
id: "sig-1",
|
|
41
|
+
label: "Treasury Liquidation",
|
|
42
|
+
primaryValue: "-$1,724,310.11",
|
|
43
|
+
qualifier: "100% of balance",
|
|
44
|
+
counterparty: "-> JPMorgan Chase --6042",
|
|
45
|
+
time: "7h ago",
|
|
46
|
+
...overrides,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeBucket(overrides: Partial<SignalScoreExplanationBucket> = {}): SignalScoreExplanationBucket {
|
|
51
|
+
return {
|
|
52
|
+
key: "treasury_liquidation",
|
|
53
|
+
label: "Treasury Liquidation",
|
|
54
|
+
kind: "signal",
|
|
55
|
+
signalCount: 2,
|
|
56
|
+
icon: "trending-down",
|
|
57
|
+
tone: "alert",
|
|
58
|
+
signals: [
|
|
59
|
+
makeSignal({ id: "sig-1" }),
|
|
60
|
+
makeSignal({ id: "sig-2", primaryValue: "-$500,000.00", qualifier: "45% of balance" }),
|
|
61
|
+
],
|
|
62
|
+
...overrides,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeSignalData(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
|
|
67
|
+
return {
|
|
68
|
+
score: 85,
|
|
69
|
+
factors: [],
|
|
70
|
+
whyNow: "Large treasury movement",
|
|
71
|
+
evidence: [],
|
|
72
|
+
confidence: 90,
|
|
73
|
+
explanationBuckets: [makeBucket()],
|
|
74
|
+
...overrides,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── ScoreWhyChips basic rendering ──────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("ScoreWhyChips", () => {
|
|
81
|
+
it("renders nothing when there are no buckets", () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<ScoreWhyChips
|
|
84
|
+
item={mockItem}
|
|
85
|
+
signalData={makeSignalData({ explanationBuckets: [] })}
|
|
86
|
+
/>,
|
|
87
|
+
)
|
|
88
|
+
expect(container.innerHTML).toBe("")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("renders the Why label and bucket pills", () => {
|
|
92
|
+
render(
|
|
93
|
+
<ScoreWhyChips
|
|
94
|
+
item={mockItem}
|
|
95
|
+
signalData={makeSignalData()}
|
|
96
|
+
/>,
|
|
97
|
+
)
|
|
98
|
+
expect(screen.getByText("Why")).toBeDefined()
|
|
99
|
+
expect(screen.getByText("Treasury Liquidation")).toBeDefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("renders count badge on pill when signalCount > 1", () => {
|
|
103
|
+
render(
|
|
104
|
+
<ScoreWhyChips
|
|
105
|
+
item={mockItem}
|
|
106
|
+
signalData={makeSignalData()}
|
|
107
|
+
/>,
|
|
108
|
+
)
|
|
109
|
+
expect(screen.getByText("x2")).toBeDefined()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("does not render count badge when signalCount is 1", () => {
|
|
113
|
+
render(
|
|
114
|
+
<ScoreWhyChips
|
|
115
|
+
item={mockItem}
|
|
116
|
+
signalData={makeSignalData({
|
|
117
|
+
explanationBuckets: [makeBucket({ signalCount: 1, signals: [makeSignal()] })],
|
|
118
|
+
})}
|
|
119
|
+
/>,
|
|
120
|
+
)
|
|
121
|
+
expect(screen.queryByText("x1")).toBeNull()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ─── Accordion behavior ────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe("ScoreWhyChips accordion", () => {
|
|
128
|
+
it("expands a bucket when pill is clicked", () => {
|
|
129
|
+
render(
|
|
130
|
+
<ScoreWhyChips
|
|
131
|
+
item={mockItem}
|
|
132
|
+
signalData={makeSignalData()}
|
|
133
|
+
/>,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Click the pill
|
|
137
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
138
|
+
|
|
139
|
+
// Should show the card with signal count header
|
|
140
|
+
expect(screen.getByText(/2 signals/)).toBeDefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("collapses when the same pill is clicked again", () => {
|
|
144
|
+
render(
|
|
145
|
+
<ScoreWhyChips
|
|
146
|
+
item={mockItem}
|
|
147
|
+
signalData={makeSignalData()}
|
|
148
|
+
/>,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const pill = screen.getByText("Treasury Liquidation")
|
|
152
|
+
fireEvent.click(pill)
|
|
153
|
+
expect(screen.getByText(/2 signals/)).toBeDefined()
|
|
154
|
+
|
|
155
|
+
// Click again to collapse
|
|
156
|
+
fireEvent.click(pill)
|
|
157
|
+
expect(screen.queryByText(/2 signals/)).toBeNull()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("only one pill is expanded at a time (accordion)", () => {
|
|
161
|
+
const bucket2 = makeBucket({
|
|
162
|
+
key: "test_transaction",
|
|
163
|
+
label: "Test Transaction",
|
|
164
|
+
icon: "radar",
|
|
165
|
+
tone: "info",
|
|
166
|
+
signalCount: 1,
|
|
167
|
+
signals: [makeSignal({ id: "sig-3", label: "Test Tx" })],
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<ScoreWhyChips
|
|
172
|
+
item={mockItem}
|
|
173
|
+
signalData={makeSignalData({
|
|
174
|
+
explanationBuckets: [makeBucket(), bucket2],
|
|
175
|
+
})}
|
|
176
|
+
/>,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
// Expand first bucket
|
|
180
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
181
|
+
expect(screen.getByText(/2 signals/)).toBeDefined()
|
|
182
|
+
|
|
183
|
+
// Expand second bucket - should collapse first
|
|
184
|
+
fireEvent.click(screen.getByText("Test Transaction"))
|
|
185
|
+
expect(screen.getByText(/1 signal/)).toBeDefined()
|
|
186
|
+
// The first bucket's card should be gone
|
|
187
|
+
expect(screen.queryByText(/2 signals – Treasury Liquidation/)).toBeNull()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("X close button collapses the expanded bucket", () => {
|
|
191
|
+
render(
|
|
192
|
+
<ScoreWhyChips
|
|
193
|
+
item={mockItem}
|
|
194
|
+
signalData={makeSignalData()}
|
|
195
|
+
/>,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Expand
|
|
199
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
200
|
+
expect(screen.getByText(/2 signals/)).toBeDefined()
|
|
201
|
+
|
|
202
|
+
// Click the X close button
|
|
203
|
+
const closeButton = screen.getByLabelText("Close Treasury Liquidation")
|
|
204
|
+
fireEvent.click(closeButton)
|
|
205
|
+
expect(screen.queryByText(/2 signals/)).toBeNull()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// ─── Structured signal row (slot grammar) ──────────────────────────────────
|
|
210
|
+
|
|
211
|
+
describe("Structured signal row", () => {
|
|
212
|
+
it("renders primary value, qualifier, counterparty, and time", () => {
|
|
213
|
+
render(
|
|
214
|
+
<ScoreWhyChips
|
|
215
|
+
item={mockItem}
|
|
216
|
+
signalData={makeSignalData()}
|
|
217
|
+
/>,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// Expand bucket
|
|
221
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
222
|
+
|
|
223
|
+
// Check structured data renders
|
|
224
|
+
expect(screen.getAllByText("-$1,724,310.11").length).toBeGreaterThanOrEqual(1)
|
|
225
|
+
expect(screen.getAllByText("100% of balance").length).toBeGreaterThanOrEqual(1)
|
|
226
|
+
expect(screen.getAllByText("-> JPMorgan Chase --6042").length).toBeGreaterThanOrEqual(1)
|
|
227
|
+
expect(screen.getAllByText("7h ago").length).toBeGreaterThanOrEqual(1)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("renders empty string for null/undefined slots (no em-dashes)", () => {
|
|
231
|
+
// Mix of present and missing data to trigger structured row rendering.
|
|
232
|
+
// One signal has a primaryValue (triggering structured mode), the other
|
|
233
|
+
// has missing fields that should render as empty.
|
|
234
|
+
render(
|
|
235
|
+
<ScoreWhyChips
|
|
236
|
+
item={mockItem}
|
|
237
|
+
signalData={makeSignalData({
|
|
238
|
+
explanationBuckets: [
|
|
239
|
+
makeBucket({
|
|
240
|
+
signals: [
|
|
241
|
+
makeSignal({
|
|
242
|
+
id: "sig-partial",
|
|
243
|
+
primaryValue: "-$100.00",
|
|
244
|
+
qualifier: undefined,
|
|
245
|
+
counterparty: undefined,
|
|
246
|
+
time: undefined,
|
|
247
|
+
}),
|
|
248
|
+
],
|
|
249
|
+
signalCount: 1,
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
})}
|
|
253
|
+
/>,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Expand bucket
|
|
257
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
258
|
+
|
|
259
|
+
// The primary value should render, but missing slots should be empty (no em-dashes)
|
|
260
|
+
expect(screen.getByText("-$100.00")).toBeTruthy()
|
|
261
|
+
// Verify no em-dashes are rendered
|
|
262
|
+
expect(screen.queryByText("\u2014")).toBeNull()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("calls onOpenSignalBucket when a signal row is clicked", () => {
|
|
266
|
+
const onOpen = vi.fn()
|
|
267
|
+
render(
|
|
268
|
+
<ScoreWhyChips
|
|
269
|
+
item={mockItem}
|
|
270
|
+
signalData={makeSignalData()}
|
|
271
|
+
onOpenSignalBucket={onOpen}
|
|
272
|
+
/>,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Expand bucket
|
|
276
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
277
|
+
|
|
278
|
+
// Find signal row buttons and click the first one
|
|
279
|
+
const signalButtons = screen.getAllByRole("button").filter(
|
|
280
|
+
(btn) => btn.className.includes("grid"),
|
|
281
|
+
)
|
|
282
|
+
expect(signalButtons.length).toBeGreaterThan(0)
|
|
283
|
+
fireEvent.click(signalButtons[0])
|
|
284
|
+
|
|
285
|
+
expect(onOpen).toHaveBeenCalledWith({
|
|
286
|
+
item: mockItem,
|
|
287
|
+
bucketKey: "treasury_liquidation",
|
|
288
|
+
signalId: "sig-1",
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// ─── Long list truncation ──────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe("Long list truncation", () => {
|
|
296
|
+
it("shows first 8 rows and 'Show N more' button when there are more than 8 signals", () => {
|
|
297
|
+
const signals = Array.from({ length: 12 }, (_, i) =>
|
|
298
|
+
makeSignal({ id: `sig-${i}`, primaryValue: `$${i * 100}` }),
|
|
299
|
+
)
|
|
300
|
+
render(
|
|
301
|
+
<ScoreWhyChips
|
|
302
|
+
item={mockItem}
|
|
303
|
+
signalData={makeSignalData({
|
|
304
|
+
explanationBuckets: [
|
|
305
|
+
makeBucket({ signals, signalCount: 12 }),
|
|
306
|
+
],
|
|
307
|
+
})}
|
|
308
|
+
/>,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
// Expand
|
|
312
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
313
|
+
|
|
314
|
+
// Should show "Show 4 more" button
|
|
315
|
+
expect(screen.getByText("Show 4 more")).toBeDefined()
|
|
316
|
+
|
|
317
|
+
// Only 8 rows visible initially (check list items)
|
|
318
|
+
const list = screen.getByRole("list", { name: "Matching signals" })
|
|
319
|
+
expect(list.children.length).toBe(8)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it("clicking 'Show N more' reveals all rows", () => {
|
|
323
|
+
const signals = Array.from({ length: 12 }, (_, i) =>
|
|
324
|
+
makeSignal({ id: `sig-${i}`, primaryValue: `$${i * 100}` }),
|
|
325
|
+
)
|
|
326
|
+
render(
|
|
327
|
+
<ScoreWhyChips
|
|
328
|
+
item={mockItem}
|
|
329
|
+
signalData={makeSignalData({
|
|
330
|
+
explanationBuckets: [
|
|
331
|
+
makeBucket({ signals, signalCount: 12 }),
|
|
332
|
+
],
|
|
333
|
+
})}
|
|
334
|
+
/>,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
338
|
+
fireEvent.click(screen.getByText("Show 4 more"))
|
|
339
|
+
|
|
340
|
+
// All 12 rows visible now
|
|
341
|
+
const list = screen.getByRole("list", { name: "Matching signals" })
|
|
342
|
+
expect(list.children.length).toBe(12)
|
|
343
|
+
|
|
344
|
+
// Button should be gone
|
|
345
|
+
expect(screen.queryByText("Show 4 more")).toBeNull()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it("does not show 'Show more' button when signals <= 8", () => {
|
|
349
|
+
render(
|
|
350
|
+
<ScoreWhyChips
|
|
351
|
+
item={mockItem}
|
|
352
|
+
signalData={makeSignalData()}
|
|
353
|
+
/>,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
357
|
+
expect(screen.queryByText(/Show \d+ more/)).toBeNull()
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// ─── Combined signal row ───────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
describe("Combined signal row", () => {
|
|
364
|
+
it("renders component mini-chips for combined_signal type", () => {
|
|
365
|
+
const combinedSignal: SignalScoreExplanationSignal = {
|
|
366
|
+
id: "sig-combined",
|
|
367
|
+
label: "Combined signal",
|
|
368
|
+
signalTypeName: "combined_signal",
|
|
369
|
+
components: [
|
|
370
|
+
{ type: "treasury_liquidation", count: 3 },
|
|
371
|
+
{ type: "test_transaction", count: 2 },
|
|
372
|
+
],
|
|
373
|
+
time: "1h ago",
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
render(
|
|
377
|
+
<ScoreWhyChips
|
|
378
|
+
item={mockItem}
|
|
379
|
+
signalData={makeSignalData({
|
|
380
|
+
explanationBuckets: [
|
|
381
|
+
makeBucket({
|
|
382
|
+
key: "combined_signal",
|
|
383
|
+
label: "Combined",
|
|
384
|
+
tone: "alert",
|
|
385
|
+
signals: [combinedSignal],
|
|
386
|
+
signalCount: 1,
|
|
387
|
+
}),
|
|
388
|
+
],
|
|
389
|
+
})}
|
|
390
|
+
/>,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
// Expand
|
|
394
|
+
fireEvent.click(screen.getByText("Combined"))
|
|
395
|
+
|
|
396
|
+
// Should render component mini-chips
|
|
397
|
+
expect(screen.getByText(/treasury liquidation x3/)).toBeDefined()
|
|
398
|
+
expect(screen.getByText(/test transaction x2/)).toBeDefined()
|
|
399
|
+
// Should render the '+' separator
|
|
400
|
+
expect(screen.getByText("+")).toBeDefined()
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// ─── Bucket feedback footer ────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
describe("Bucket feedback footer", () => {
|
|
407
|
+
it("renders feedback footer when onBucketFeedback is provided", () => {
|
|
408
|
+
const onBucketFeedback = vi.fn()
|
|
409
|
+
render(
|
|
410
|
+
<ScoreWhyChips
|
|
411
|
+
item={mockItem}
|
|
412
|
+
signalData={makeSignalData({ onBucketFeedback })}
|
|
413
|
+
/>,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
// Expand bucket
|
|
417
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
418
|
+
|
|
419
|
+
// Should show thumbs buttons
|
|
420
|
+
expect(screen.getByText("Helpful")).toBeDefined()
|
|
421
|
+
expect(screen.getByText("Not helpful")).toBeDefined()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it("does not render feedback footer when onBucketFeedback is not provided", () => {
|
|
425
|
+
render(
|
|
426
|
+
<ScoreWhyChips
|
|
427
|
+
item={mockItem}
|
|
428
|
+
signalData={makeSignalData()}
|
|
429
|
+
/>,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Expand bucket
|
|
433
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
434
|
+
|
|
435
|
+
// Should NOT show thumbs buttons
|
|
436
|
+
expect(screen.queryByText("Helpful")).toBeNull()
|
|
437
|
+
expect(screen.queryByText("Not helpful")).toBeNull()
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it("fires onBucketFeedback with correct bucket key and FeedbackSubmitData", () => {
|
|
441
|
+
const onBucketFeedback = vi.fn()
|
|
442
|
+
render(
|
|
443
|
+
<ScoreWhyChips
|
|
444
|
+
item={mockItem}
|
|
445
|
+
signalData={makeSignalData({ onBucketFeedback })}
|
|
446
|
+
/>,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// Expand bucket
|
|
450
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
451
|
+
|
|
452
|
+
// Click "Not helpful"
|
|
453
|
+
fireEvent.click(screen.getByText("Not helpful"))
|
|
454
|
+
|
|
455
|
+
// Select a chip
|
|
456
|
+
fireEvent.click(screen.getByText("Bad timing"))
|
|
457
|
+
|
|
458
|
+
// Submit
|
|
459
|
+
fireEvent.click(screen.getByText("Submit"))
|
|
460
|
+
|
|
461
|
+
expect(onBucketFeedback).toHaveBeenCalledTimes(1)
|
|
462
|
+
expect(onBucketFeedback).toHaveBeenCalledWith("treasury_liquidation", {
|
|
463
|
+
sentiment: "negative",
|
|
464
|
+
reasonTop: "Bad timing",
|
|
465
|
+
reasonSub: undefined,
|
|
466
|
+
pills: [],
|
|
467
|
+
detail: "",
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// ─── WhyPill visual states ─────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
describe("WhyPill visual states", () => {
|
|
475
|
+
it("collapsed pill has rounded-lg and bg-background classes", () => {
|
|
476
|
+
render(
|
|
477
|
+
<ScoreWhyChips
|
|
478
|
+
item={mockItem}
|
|
479
|
+
signalData={makeSignalData()}
|
|
480
|
+
/>,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
const pill = screen.getByRole("button", { name: /Treasury Liquidation/i })
|
|
484
|
+
expect(pill.className).toContain("rounded-lg")
|
|
485
|
+
expect(pill.className).toContain("bg-background")
|
|
486
|
+
expect(pill.className).not.toContain("rounded-b-none")
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it("expanded pill has rounded-b-none and bg-muted classes", () => {
|
|
490
|
+
render(
|
|
491
|
+
<ScoreWhyChips
|
|
492
|
+
item={mockItem}
|
|
493
|
+
signalData={makeSignalData()}
|
|
494
|
+
/>,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
// Expand
|
|
498
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
499
|
+
|
|
500
|
+
const pill = screen.getByRole("button", { expanded: true })
|
|
501
|
+
expect(pill.className).toContain("rounded-b-none")
|
|
502
|
+
expect(pill.className).toContain("bg-muted")
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
// ─── Legacy signal row fallback ────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
describe("Legacy signal row fallback", () => {
|
|
509
|
+
it("renders legacy rows when signals lack structured data", () => {
|
|
510
|
+
const legacySignal: SignalScoreExplanationSignal = {
|
|
511
|
+
id: "sig-legacy",
|
|
512
|
+
label: "Old format signal",
|
|
513
|
+
description: "Some legacy description",
|
|
514
|
+
source: "Bank API",
|
|
515
|
+
metric: "$500K",
|
|
516
|
+
time: "3h ago",
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
render(
|
|
520
|
+
<ScoreWhyChips
|
|
521
|
+
item={mockItem}
|
|
522
|
+
signalData={makeSignalData({
|
|
523
|
+
explanationBuckets: [
|
|
524
|
+
makeBucket({
|
|
525
|
+
signals: [legacySignal],
|
|
526
|
+
signalCount: 1,
|
|
527
|
+
}),
|
|
528
|
+
],
|
|
529
|
+
})}
|
|
530
|
+
/>,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
534
|
+
|
|
535
|
+
expect(screen.getByText("Old format signal")).toBeDefined()
|
|
536
|
+
expect(screen.getByText("Some legacy description")).toBeDefined()
|
|
537
|
+
expect(screen.getByText("Bank API")).toBeDefined()
|
|
538
|
+
expect(screen.getByText("$500K")).toBeDefined()
|
|
539
|
+
})
|
|
540
|
+
})
|