@handled-ai/design-system 0.18.3 → 0.18.4
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/chart.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/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/timeline-activity.d.ts +1 -16
- package/dist/components/timeline-activity.js +1 -69
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +3 -3
- 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 +2 -12
- package/dist/prototype/prototype-inbox-view.js +37 -102
- 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 +3 -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/components/timeline-activity.tsx +1 -112
- package/src/index.ts +2 -2
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/prototype-config.ts +11 -1
- package/src/prototype/prototype-inbox-view.tsx +33 -131
- package/src/components/__tests__/timeline-activity.test.tsx +0 -137
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +0 -322
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { h as InsightsViewConfig } from '../signal-priority-popover-
|
|
2
|
+
import { h as InsightsViewConfig } from '../signal-priority-popover-DWaAMhPI.js';
|
|
3
3
|
import '../components/feedback-primitives.js';
|
|
4
4
|
import '../components/quick-action-sidebar-nav.js';
|
|
5
5
|
import '../components/quick-action-modal.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { j as PrototypeConfig } from '../signal-priority-popover-
|
|
2
|
+
import { j as PrototypeConfig } from '../signal-priority-popover-DWaAMhPI.js';
|
|
3
3
|
import '../components/feedback-primitives.js';
|
|
4
4
|
import '../components/quick-action-sidebar-nav.js';
|
|
5
5
|
import '../components/quick-action-modal.js';
|
package/dist/{signal-priority-popover-DQ_VuHac.d.ts → signal-priority-popover-DWaAMhPI.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { FeedbackChipTree, FeedbackSubmitData } from './components/feedback-primitives.js';
|
|
2
|
+
import { PersistedFeedbackData, FeedbackChipTree, FeedbackSubmitData } from './components/feedback-primitives.js';
|
|
3
3
|
import { SidebarNavSection } from './components/quick-action-sidebar-nav.js';
|
|
4
4
|
import { ScoreFactor } from './components/score-breakdown.js';
|
|
5
5
|
import { SuggestedContact, SuggestedAction } from './components/suggested-actions.js';
|
|
@@ -48,6 +48,10 @@ interface SignalScoreExplanationSignal {
|
|
|
48
48
|
type: string;
|
|
49
49
|
count: number;
|
|
50
50
|
}>;
|
|
51
|
+
/** Current balance value (e.g., "$3.0M") for balance context strip. */
|
|
52
|
+
currentBalance?: string;
|
|
53
|
+
/** Additional balance context text (e.g., "down from $23M"). */
|
|
54
|
+
balanceContext?: string;
|
|
51
55
|
}
|
|
52
56
|
interface SignalScoreExplanationBucket {
|
|
53
57
|
key: string;
|
|
@@ -93,6 +97,16 @@ interface SignalScoreData {
|
|
|
93
97
|
type: "up" | "down";
|
|
94
98
|
detail: string;
|
|
95
99
|
}>;
|
|
100
|
+
/** Factor-level feedback for the priority popover rows (keyed by factor key). */
|
|
101
|
+
initialFactorPopoverFeedback?: Record<string, {
|
|
102
|
+
type: "up" | "down";
|
|
103
|
+
detail: string;
|
|
104
|
+
ownershipLabel?: string;
|
|
105
|
+
}>;
|
|
106
|
+
/** Persisted bucket-level feedback, keyed by bucket key. */
|
|
107
|
+
initialBucketFeedback?: Record<string, PersistedFeedbackData>;
|
|
108
|
+
/** Persisted priority-level feedback for the popover footer. */
|
|
109
|
+
initialPriorityFeedback?: PersistedFeedbackData | null;
|
|
96
110
|
/** Priority factors for the popover breakdown. */
|
|
97
111
|
priorityFactors?: PriorityFactor[];
|
|
98
112
|
/** Negative feedback chip tree for the priority popover. */
|
|
@@ -384,7 +398,17 @@ interface SignalPriorityPopoverProps {
|
|
|
384
398
|
feedbackChips?: FeedbackChipTree[];
|
|
385
399
|
onFeedbackSubmit?: (data: FeedbackSubmitData) => void;
|
|
386
400
|
className?: string;
|
|
401
|
+
/** Persisted factor-level feedback (keyed by factor key). */
|
|
402
|
+
initialFactorFeedback?: Record<string, {
|
|
403
|
+
type: "up" | "down";
|
|
404
|
+
detail: string;
|
|
405
|
+
ownershipLabel?: string;
|
|
406
|
+
}>;
|
|
407
|
+
/** Callback when user submits factor-level feedback. */
|
|
408
|
+
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void;
|
|
409
|
+
/** Persisted priority-level feedback for the footer. */
|
|
410
|
+
initialPriorityFeedback?: PersistedFeedbackData | null;
|
|
387
411
|
}
|
|
388
|
-
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, }: SignalPriorityPopoverProps): React.JSX.Element;
|
|
412
|
+
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, initialFactorFeedback, onFactorFeedback, initialPriorityFeedback, }: SignalPriorityPopoverProps): React.JSX.Element;
|
|
389
413
|
|
|
390
414
|
export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handled-ai/design-system",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"description": "Handled UI component library (shadcn-style, New York)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@9.12.0",
|
|
@@ -171,9 +171,11 @@
|
|
|
171
171
|
"eslint": "^9.32.0",
|
|
172
172
|
"eslint-config-next": "15.3.1",
|
|
173
173
|
"happy-dom": "^20.9.0",
|
|
174
|
+
"lucide-react": "^1.16.0",
|
|
174
175
|
"next": "15.5.9",
|
|
175
176
|
"react": "19.1.0",
|
|
176
177
|
"react-dom": "19.1.0",
|
|
178
|
+
"recharts": "^3.8.1",
|
|
177
179
|
"shadcn": "^3.0.0",
|
|
178
180
|
"tailwindcss": "^4.1.11",
|
|
179
181
|
"three": "^0.183.1",
|
|
@@ -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
|
+
})
|