@handled-ai/design-system 0.18.5 → 0.18.6
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/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/feedback-primitives.d.ts +41 -2
- package/dist/components/feedback-primitives.js +241 -6
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +26 -5
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +32 -6
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.js +4 -1
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-DWaAMhPI.d.ts} +26 -2
- package/package.json +1 -1
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +546 -0
- package/src/components/feedback-primitives.tsx +333 -26
- package/src/components/score-why-chips.tsx +28 -2
- package/src/components/signal-priority-popover.tsx +44 -4
- package/src/index.ts +2 -2
- package/src/prototype/prototype-config.ts +11 -1
- package/src/prototype/prototype-inbox-view.tsx +3 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for WIT-636: Feedback confirmation and persisted-state support.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - SignalPriorityPopover factor feedback with no note shows `Saved`
|
|
6
|
+
* - SignalPriorityPopover factor feedback with existing current-user note shows `Your feedback:` after initial render and after prop updates
|
|
7
|
+
* - SignalPriorityPopover factor feedback with teammate fallback shows `Team feedback:`
|
|
8
|
+
* - Priority popover footer initial feedback hydrates and submit confirmation appears
|
|
9
|
+
* - Bucket footer submit shows saved/submitted state
|
|
10
|
+
* - Bucket initial feedback shows `Your feedback:` or `Team feedback:`
|
|
11
|
+
* - Footer syncs when initialFeedback arrives after mount, switches when feedbackKey changes, does not overwrite active edits for same key
|
|
12
|
+
* - Balance strip renders for structured WHY rows
|
|
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 { FeedbackFooter } from "../feedback-primitives"
|
|
19
|
+
import type { PersistedFeedbackData } from "../feedback-primitives"
|
|
20
|
+
import { SignalPriorityPopover } from "../signal-priority-popover"
|
|
21
|
+
import type { PriorityFactor } from "../signal-priority-popover"
|
|
22
|
+
import { ScoreWhyChips } from "../score-why-chips"
|
|
23
|
+
import type {
|
|
24
|
+
QueueItem,
|
|
25
|
+
SignalScoreData,
|
|
26
|
+
SignalScoreExplanationBucket,
|
|
27
|
+
SignalScoreExplanationSignal,
|
|
28
|
+
} from "../../prototype/prototype-config"
|
|
29
|
+
|
|
30
|
+
// ─── Shared mock data ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const mockFactors: PriorityFactor[] = [
|
|
33
|
+
{
|
|
34
|
+
key: "timing-asymmetry",
|
|
35
|
+
label: "Timing asymmetry",
|
|
36
|
+
icon: "radar",
|
|
37
|
+
tone: "alert",
|
|
38
|
+
direction: "raises",
|
|
39
|
+
score: 92,
|
|
40
|
+
rationale: "Test transaction 12h ago → no follow-on liquidation yet.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "funds-at-stake",
|
|
44
|
+
label: "Funds at stake",
|
|
45
|
+
icon: "wallet",
|
|
46
|
+
tone: "alert",
|
|
47
|
+
direction: "raises",
|
|
48
|
+
score: 88,
|
|
49
|
+
rationale: "$3.4M moved in 8h · current treasury balance $0.00",
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const mockItem: QueueItem = {
|
|
54
|
+
id: "case-1",
|
|
55
|
+
title: "Test Case",
|
|
56
|
+
details: "Some details",
|
|
57
|
+
statusColor: "red",
|
|
58
|
+
time: "2h ago",
|
|
59
|
+
company: "TestCorp",
|
|
60
|
+
tag1: "urgent",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeSignal(overrides: Partial<SignalScoreExplanationSignal> = {}): SignalScoreExplanationSignal {
|
|
64
|
+
return {
|
|
65
|
+
id: "sig-1",
|
|
66
|
+
label: "Treasury Liquidation",
|
|
67
|
+
primaryValue: "-$1,724,310.11",
|
|
68
|
+
qualifier: "100% of balance",
|
|
69
|
+
counterparty: "-> JPMorgan Chase --6042",
|
|
70
|
+
time: "7h ago",
|
|
71
|
+
...overrides,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeBucket(overrides: Partial<SignalScoreExplanationBucket> = {}): SignalScoreExplanationBucket {
|
|
76
|
+
return {
|
|
77
|
+
key: "treasury_liquidation",
|
|
78
|
+
label: "Treasury Liquidation",
|
|
79
|
+
kind: "signal",
|
|
80
|
+
signalCount: 2,
|
|
81
|
+
icon: "trending-down",
|
|
82
|
+
tone: "alert",
|
|
83
|
+
signals: [
|
|
84
|
+
makeSignal({ id: "sig-1" }),
|
|
85
|
+
makeSignal({ id: "sig-2", primaryValue: "-$500,000.00", qualifier: "45% of balance" }),
|
|
86
|
+
],
|
|
87
|
+
...overrides,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeSignalData(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
|
|
92
|
+
return {
|
|
93
|
+
score: 85,
|
|
94
|
+
factors: [],
|
|
95
|
+
whyNow: "Large treasury movement",
|
|
96
|
+
evidence: [],
|
|
97
|
+
confidence: 90,
|
|
98
|
+
explanationBuckets: [makeBucket()],
|
|
99
|
+
...overrides,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── SignalPriorityPopover factor feedback ──────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe("SignalPriorityPopover factor feedback", () => {
|
|
106
|
+
it("factor feedback with no note shows Saved (persisted state)", () => {
|
|
107
|
+
const onFactorFeedback = vi.fn()
|
|
108
|
+
render(
|
|
109
|
+
<SignalPriorityPopover
|
|
110
|
+
score={79}
|
|
111
|
+
urgencyLabel="High"
|
|
112
|
+
factors={mockFactors}
|
|
113
|
+
onFeedbackSubmit={vi.fn()}
|
|
114
|
+
onFactorFeedback={onFactorFeedback}
|
|
115
|
+
/>,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Open popover
|
|
119
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
120
|
+
|
|
121
|
+
// Click thumb-up on first factor
|
|
122
|
+
fireEvent.click(screen.getByTestId("factor-thumb-up-timing-asymmetry"))
|
|
123
|
+
|
|
124
|
+
// Submit without typing a note
|
|
125
|
+
fireEvent.click(screen.getByTestId("factor-submit-timing-asymmetry"))
|
|
126
|
+
|
|
127
|
+
// After submit, factor transitions to persisted state showing "Your feedback:"
|
|
128
|
+
const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
|
|
129
|
+
expect(indicator).toBeTruthy()
|
|
130
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
131
|
+
|
|
132
|
+
// Callback should have been called
|
|
133
|
+
expect(onFactorFeedback).toHaveBeenCalledWith("timing-asymmetry", "up", "")
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("factor feedback with existing current-user note shows 'Your feedback:' after initial render", () => {
|
|
137
|
+
const initialFactorFeedback = {
|
|
138
|
+
"timing-asymmetry": { type: "down" as const, detail: "Score is too high", ownershipLabel: "Your feedback" },
|
|
139
|
+
}
|
|
140
|
+
render(
|
|
141
|
+
<SignalPriorityPopover
|
|
142
|
+
score={79}
|
|
143
|
+
urgencyLabel="High"
|
|
144
|
+
factors={mockFactors}
|
|
145
|
+
onFeedbackSubmit={vi.fn()}
|
|
146
|
+
onFactorFeedback={vi.fn()}
|
|
147
|
+
initialFactorFeedback={initialFactorFeedback}
|
|
148
|
+
/>,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Open popover
|
|
152
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
153
|
+
|
|
154
|
+
// Should show persisted indicator with "Your feedback:"
|
|
155
|
+
const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
|
|
156
|
+
expect(indicator).toBeTruthy()
|
|
157
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
158
|
+
expect(indicator.textContent).toContain("Score is too high")
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("factor feedback with existing current-user note shows 'Your feedback:' after prop updates", () => {
|
|
162
|
+
const onFactorFeedback = vi.fn()
|
|
163
|
+
|
|
164
|
+
const { rerender } = render(
|
|
165
|
+
<SignalPriorityPopover
|
|
166
|
+
score={79}
|
|
167
|
+
urgencyLabel="High"
|
|
168
|
+
factors={mockFactors}
|
|
169
|
+
onFeedbackSubmit={vi.fn()}
|
|
170
|
+
onFactorFeedback={onFactorFeedback}
|
|
171
|
+
/>,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Open popover
|
|
175
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
176
|
+
|
|
177
|
+
// Initially no persisted feedback
|
|
178
|
+
expect(screen.queryByTestId("factor-feedback-persisted-timing-asymmetry")).toBeNull()
|
|
179
|
+
|
|
180
|
+
// Rerender with initialFactorFeedback
|
|
181
|
+
rerender(
|
|
182
|
+
<SignalPriorityPopover
|
|
183
|
+
score={79}
|
|
184
|
+
urgencyLabel="High"
|
|
185
|
+
factors={mockFactors}
|
|
186
|
+
onFeedbackSubmit={vi.fn()}
|
|
187
|
+
onFactorFeedback={onFactorFeedback}
|
|
188
|
+
initialFactorFeedback={{
|
|
189
|
+
"timing-asymmetry": { type: "up", detail: "Looks correct", ownershipLabel: "Your feedback" },
|
|
190
|
+
}}
|
|
191
|
+
/>,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Now should show persisted indicator
|
|
195
|
+
const indicator = screen.getByTestId("factor-feedback-persisted-timing-asymmetry")
|
|
196
|
+
expect(indicator).toBeTruthy()
|
|
197
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
198
|
+
expect(indicator.textContent).toContain("Looks correct")
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("factor feedback with teammate fallback shows 'Team feedback:'", () => {
|
|
202
|
+
const initialFactorFeedback = {
|
|
203
|
+
"funds-at-stake": { type: "up" as const, detail: "Accurate numbers", ownershipLabel: "Team feedback" },
|
|
204
|
+
}
|
|
205
|
+
render(
|
|
206
|
+
<SignalPriorityPopover
|
|
207
|
+
score={79}
|
|
208
|
+
urgencyLabel="High"
|
|
209
|
+
factors={mockFactors}
|
|
210
|
+
onFeedbackSubmit={vi.fn()}
|
|
211
|
+
onFactorFeedback={vi.fn()}
|
|
212
|
+
initialFactorFeedback={initialFactorFeedback}
|
|
213
|
+
/>,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// Open popover
|
|
217
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
218
|
+
|
|
219
|
+
// Should show "Team feedback:" for the funds-at-stake factor
|
|
220
|
+
const indicator = screen.getByTestId("factor-feedback-persisted-funds-at-stake")
|
|
221
|
+
expect(indicator).toBeTruthy()
|
|
222
|
+
expect(indicator.textContent).toContain("Team feedback:")
|
|
223
|
+
expect(indicator.textContent).toContain("Accurate numbers")
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// ─── Priority popover footer initial feedback ────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe("Priority popover footer initial feedback", () => {
|
|
230
|
+
it("hydrates from initialPriorityFeedback and submit confirmation appears", () => {
|
|
231
|
+
const initialPriorityFeedback: PersistedFeedbackData = {
|
|
232
|
+
sentiment: "negative",
|
|
233
|
+
reasonTop: "Wrong factor weighting",
|
|
234
|
+
detail: "Last-contact weight is too high",
|
|
235
|
+
ownershipLabel: "Your feedback",
|
|
236
|
+
}
|
|
237
|
+
render(
|
|
238
|
+
<SignalPriorityPopover
|
|
239
|
+
score={79}
|
|
240
|
+
urgencyLabel="High"
|
|
241
|
+
factors={mockFactors}
|
|
242
|
+
onFeedbackSubmit={vi.fn()}
|
|
243
|
+
initialPriorityFeedback={initialPriorityFeedback}
|
|
244
|
+
/>,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// Open popover
|
|
248
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
249
|
+
|
|
250
|
+
// Should show persisted indicator in the footer
|
|
251
|
+
const indicator = screen.getByTestId("persisted-feedback-indicator")
|
|
252
|
+
expect(indicator).toBeTruthy()
|
|
253
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
254
|
+
expect(indicator.textContent).toContain("Last-contact weight is too high")
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// ─── Bucket footer submit shows saved/submitted state ─────────────────────
|
|
259
|
+
|
|
260
|
+
describe("Bucket feedback footer", () => {
|
|
261
|
+
it("submit shows saved/submitted state", () => {
|
|
262
|
+
const onBucketFeedback = vi.fn()
|
|
263
|
+
render(
|
|
264
|
+
<ScoreWhyChips
|
|
265
|
+
item={mockItem}
|
|
266
|
+
signalData={makeSignalData({ onBucketFeedback })}
|
|
267
|
+
/>,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
// Expand bucket
|
|
271
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
272
|
+
|
|
273
|
+
// Click "Not helpful"
|
|
274
|
+
fireEvent.click(screen.getByText("Not helpful"))
|
|
275
|
+
|
|
276
|
+
// Submit
|
|
277
|
+
fireEvent.click(screen.getByText("Submit"))
|
|
278
|
+
|
|
279
|
+
// Should show "Saved" pill
|
|
280
|
+
const savedPill = screen.getByTestId("feedback-submitted-pill")
|
|
281
|
+
expect(savedPill).toBeTruthy()
|
|
282
|
+
expect(savedPill.textContent).toContain("Saved")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("bucket initial feedback shows 'Your feedback:'", () => {
|
|
286
|
+
const onBucketFeedback = vi.fn()
|
|
287
|
+
const initialBucketFeedback: Record<string, PersistedFeedbackData> = {
|
|
288
|
+
treasury_liquidation: {
|
|
289
|
+
sentiment: "positive",
|
|
290
|
+
detail: "Counterparty match was right",
|
|
291
|
+
ownershipLabel: "Your feedback",
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
render(
|
|
295
|
+
<ScoreWhyChips
|
|
296
|
+
item={mockItem}
|
|
297
|
+
signalData={makeSignalData({ onBucketFeedback, initialBucketFeedback })}
|
|
298
|
+
/>,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Expand bucket
|
|
302
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
303
|
+
|
|
304
|
+
// Should show persisted indicator
|
|
305
|
+
const indicator = screen.getByTestId("persisted-feedback-indicator")
|
|
306
|
+
expect(indicator).toBeTruthy()
|
|
307
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
308
|
+
expect(indicator.textContent).toContain("Counterparty match was right")
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it("bucket initial feedback shows 'Team feedback:'", () => {
|
|
312
|
+
const onBucketFeedback = vi.fn()
|
|
313
|
+
const initialBucketFeedback: Record<string, PersistedFeedbackData> = {
|
|
314
|
+
treasury_liquidation: {
|
|
315
|
+
sentiment: "negative",
|
|
316
|
+
detail: "Timing was off",
|
|
317
|
+
ownershipLabel: "Team feedback",
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
render(
|
|
321
|
+
<ScoreWhyChips
|
|
322
|
+
item={mockItem}
|
|
323
|
+
signalData={makeSignalData({ onBucketFeedback, initialBucketFeedback })}
|
|
324
|
+
/>,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
// Expand bucket
|
|
328
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
329
|
+
|
|
330
|
+
// Should show persisted indicator with team label
|
|
331
|
+
const indicator = screen.getByTestId("persisted-feedback-indicator")
|
|
332
|
+
expect(indicator).toBeTruthy()
|
|
333
|
+
expect(indicator.textContent).toContain("Team feedback:")
|
|
334
|
+
expect(indicator.textContent).toContain("Timing was off")
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// ─── FeedbackFooter sync behavior ────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
describe("FeedbackFooter sync behavior", () => {
|
|
341
|
+
it("syncs when initialFeedback arrives after mount", () => {
|
|
342
|
+
const onFeedbackChange = vi.fn()
|
|
343
|
+
const { rerender } = render(
|
|
344
|
+
<FeedbackFooter
|
|
345
|
+
feedback={null}
|
|
346
|
+
onFeedbackChange={onFeedbackChange}
|
|
347
|
+
onSubmit={vi.fn()}
|
|
348
|
+
feedbackKey="bucket-1"
|
|
349
|
+
/>,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
// Initially no persisted indicator
|
|
353
|
+
expect(screen.queryByTestId("persisted-feedback-indicator")).toBeNull()
|
|
354
|
+
expect(screen.getByText("Helpful")).toBeTruthy()
|
|
355
|
+
|
|
356
|
+
// Simulate async data arriving
|
|
357
|
+
const incoming: PersistedFeedbackData = {
|
|
358
|
+
sentiment: "positive",
|
|
359
|
+
detail: "Looks good",
|
|
360
|
+
ownershipLabel: "Your feedback",
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
rerender(
|
|
364
|
+
<FeedbackFooter
|
|
365
|
+
feedback="positive"
|
|
366
|
+
onFeedbackChange={onFeedbackChange}
|
|
367
|
+
onSubmit={vi.fn()}
|
|
368
|
+
feedbackKey="bucket-1"
|
|
369
|
+
initialFeedback={incoming}
|
|
370
|
+
/>,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
// Now should show persisted indicator
|
|
374
|
+
const indicator = screen.getByTestId("persisted-feedback-indicator")
|
|
375
|
+
expect(indicator).toBeTruthy()
|
|
376
|
+
expect(indicator.textContent).toContain("Your feedback:")
|
|
377
|
+
expect(indicator.textContent).toContain("Looks good")
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("switches when feedbackKey changes", () => {
|
|
381
|
+
const onFeedbackChange = vi.fn()
|
|
382
|
+
const fb1: PersistedFeedbackData = {
|
|
383
|
+
sentiment: "positive",
|
|
384
|
+
detail: "Good",
|
|
385
|
+
ownershipLabel: "Your feedback",
|
|
386
|
+
}
|
|
387
|
+
const fb2: PersistedFeedbackData = {
|
|
388
|
+
sentiment: "negative",
|
|
389
|
+
detail: "Bad",
|
|
390
|
+
ownershipLabel: "Team feedback",
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { rerender } = render(
|
|
394
|
+
<FeedbackFooter
|
|
395
|
+
feedback="positive"
|
|
396
|
+
onFeedbackChange={onFeedbackChange}
|
|
397
|
+
onSubmit={vi.fn()}
|
|
398
|
+
feedbackKey="bucket-1"
|
|
399
|
+
initialFeedback={fb1}
|
|
400
|
+
/>,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
// Should show fb1 content
|
|
404
|
+
expect(screen.getByTestId("persisted-feedback-indicator").textContent).toContain("Good")
|
|
405
|
+
|
|
406
|
+
// Switch to different key with different feedback
|
|
407
|
+
rerender(
|
|
408
|
+
<FeedbackFooter
|
|
409
|
+
feedback="negative"
|
|
410
|
+
onFeedbackChange={onFeedbackChange}
|
|
411
|
+
onSubmit={vi.fn()}
|
|
412
|
+
feedbackKey="bucket-2"
|
|
413
|
+
initialFeedback={fb2}
|
|
414
|
+
/>,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
// Should now show fb2 content
|
|
418
|
+
const indicator = screen.getByTestId("persisted-feedback-indicator")
|
|
419
|
+
expect(indicator.textContent).toContain("Team feedback:")
|
|
420
|
+
expect(indicator.textContent).toContain("Bad")
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it("does not overwrite active edits for same key", () => {
|
|
424
|
+
const onFeedbackChange = vi.fn()
|
|
425
|
+
const onSubmit = vi.fn()
|
|
426
|
+
const fb: PersistedFeedbackData = {
|
|
427
|
+
sentiment: "positive",
|
|
428
|
+
detail: "Initial note",
|
|
429
|
+
ownershipLabel: "Your feedback",
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const { rerender } = render(
|
|
433
|
+
<FeedbackFooter
|
|
434
|
+
feedback="positive"
|
|
435
|
+
onFeedbackChange={onFeedbackChange}
|
|
436
|
+
onSubmit={onSubmit}
|
|
437
|
+
feedbackKey="bucket-1"
|
|
438
|
+
initialFeedback={fb}
|
|
439
|
+
/>,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
// Click the persisted indicator to open editor
|
|
443
|
+
fireEvent.click(screen.getByTestId("persisted-feedback-indicator"))
|
|
444
|
+
|
|
445
|
+
// Now should be in editing mode with the expansion open
|
|
446
|
+
expect(screen.getByPlaceholderText("Add optional detail…")).toBeTruthy()
|
|
447
|
+
|
|
448
|
+
// Type new detail
|
|
449
|
+
const input = screen.getByPlaceholderText("Add optional detail…")
|
|
450
|
+
fireEvent.change(input, { target: { value: "My new note" } })
|
|
451
|
+
|
|
452
|
+
// Rerender with updated initialFeedback (simulating prop change) - same key
|
|
453
|
+
const fbUpdated: PersistedFeedbackData = {
|
|
454
|
+
sentiment: "negative",
|
|
455
|
+
detail: "Server-side update",
|
|
456
|
+
ownershipLabel: "Team feedback",
|
|
457
|
+
}
|
|
458
|
+
rerender(
|
|
459
|
+
<FeedbackFooter
|
|
460
|
+
feedback="positive"
|
|
461
|
+
onFeedbackChange={onFeedbackChange}
|
|
462
|
+
onSubmit={onSubmit}
|
|
463
|
+
feedbackKey="bucket-1"
|
|
464
|
+
initialFeedback={fbUpdated}
|
|
465
|
+
/>,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
// The editor should still be open with the user's text - not the server update
|
|
469
|
+
const editInput = screen.getByPlaceholderText("Add optional detail…")
|
|
470
|
+
expect(editInput).toBeTruthy()
|
|
471
|
+
expect((editInput as HTMLInputElement).value).toBe("My new note")
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// ─── Balance strip rendering ──────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
describe("Balance context strip", () => {
|
|
478
|
+
it("renders for structured WHY rows with currentBalance", () => {
|
|
479
|
+
const signalWithBalance = makeSignal({
|
|
480
|
+
id: "sig-balance",
|
|
481
|
+
currentBalance: "$3.0M",
|
|
482
|
+
balanceContext: "down from $23M",
|
|
483
|
+
})
|
|
484
|
+
render(
|
|
485
|
+
<ScoreWhyChips
|
|
486
|
+
item={mockItem}
|
|
487
|
+
signalData={makeSignalData({
|
|
488
|
+
explanationBuckets: [
|
|
489
|
+
makeBucket({ signals: [signalWithBalance], signalCount: 1 }),
|
|
490
|
+
],
|
|
491
|
+
})}
|
|
492
|
+
/>,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// Expand bucket
|
|
496
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
497
|
+
|
|
498
|
+
// Should render balance strip
|
|
499
|
+
const strip = screen.getByTestId("balance-context-strip")
|
|
500
|
+
expect(strip).toBeTruthy()
|
|
501
|
+
expect(strip.textContent).toContain("Current balance")
|
|
502
|
+
expect(strip.textContent).toContain("$3.0M")
|
|
503
|
+
expect(strip.textContent).toContain("down from $23M")
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it("renders only balanceContext when currentBalance is absent", () => {
|
|
507
|
+
const signalWithContext = makeSignal({
|
|
508
|
+
id: "sig-ctx",
|
|
509
|
+
balanceContext: "significant velocity",
|
|
510
|
+
})
|
|
511
|
+
render(
|
|
512
|
+
<ScoreWhyChips
|
|
513
|
+
item={mockItem}
|
|
514
|
+
signalData={makeSignalData({
|
|
515
|
+
explanationBuckets: [
|
|
516
|
+
makeBucket({ signals: [signalWithContext], signalCount: 1 }),
|
|
517
|
+
],
|
|
518
|
+
})}
|
|
519
|
+
/>,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
// Expand bucket
|
|
523
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
524
|
+
|
|
525
|
+
// Should render balance strip with only context
|
|
526
|
+
const strip = screen.getByTestId("balance-context-strip")
|
|
527
|
+
expect(strip).toBeTruthy()
|
|
528
|
+
expect(strip.textContent).toContain("significant velocity")
|
|
529
|
+
expect(strip.textContent).not.toContain("Current balance")
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it("does not render balance strip when neither currentBalance nor balanceContext is present", () => {
|
|
533
|
+
render(
|
|
534
|
+
<ScoreWhyChips
|
|
535
|
+
item={mockItem}
|
|
536
|
+
signalData={makeSignalData()}
|
|
537
|
+
/>,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
// Expand bucket
|
|
541
|
+
fireEvent.click(screen.getByText("Treasury Liquidation"))
|
|
542
|
+
|
|
543
|
+
// Should NOT render balance strip
|
|
544
|
+
expect(screen.queryByTestId("balance-context-strip")).toBeNull()
|
|
545
|
+
})
|
|
546
|
+
})
|