@startsimpli/ui 0.4.6 → 0.4.8
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/package.json +2 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { parseDateRangeFromTitle, getHierarchyLevel } from '../lib/dates'
|
|
2
|
+
import {
|
|
3
|
+
calculateExpectedProgress,
|
|
4
|
+
calculateHealthStatus,
|
|
5
|
+
getHealthColor,
|
|
6
|
+
} from '../lib/progress'
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// dates.ts
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
describe('parseDateRangeFromTitle', () => {
|
|
13
|
+
const REF_YEAR = 2025
|
|
14
|
+
|
|
15
|
+
describe('no tag prefix', () => {
|
|
16
|
+
it('returns null when title has no bracket tag', () => {
|
|
17
|
+
expect(parseDateRangeFromTitle('Plain title', REF_YEAR)).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns null for empty string', () => {
|
|
21
|
+
expect(parseDateRangeFromTitle('', REF_YEAR)).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('year range — Nyr', () => {
|
|
26
|
+
it('parses [1yr] as the reference year', () => {
|
|
27
|
+
const result = parseDateRangeFromTitle('[1yr] Company goal', REF_YEAR)
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result!.start.getFullYear()).toBe(REF_YEAR)
|
|
30
|
+
expect(result!.start.getMonth()).toBe(0)
|
|
31
|
+
expect(result!.start.getDate()).toBe(1)
|
|
32
|
+
expect(result!.end.getFullYear()).toBe(REF_YEAR)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('parses [3yr] spanning three years from the reference year', () => {
|
|
36
|
+
const result = parseDateRangeFromTitle('[3yr] Three-year plan', REF_YEAR)
|
|
37
|
+
expect(result).not.toBeNull()
|
|
38
|
+
expect(result!.start.getFullYear()).toBe(REF_YEAR)
|
|
39
|
+
expect(result!.end.getFullYear()).toBe(REF_YEAR + 2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('is case-insensitive — [2YR] works', () => {
|
|
43
|
+
const result = parseDateRangeFromTitle('[2YR] Two-year plan', REF_YEAR)
|
|
44
|
+
expect(result).not.toBeNull()
|
|
45
|
+
expect(result!.end.getFullYear()).toBe(REF_YEAR + 1)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('half-year — H1 / H2', () => {
|
|
50
|
+
it('parses [H1] as Jan 1 – Jun 30', () => {
|
|
51
|
+
const result = parseDateRangeFromTitle('[H1] First half', REF_YEAR)
|
|
52
|
+
expect(result).not.toBeNull()
|
|
53
|
+
expect(result!.start).toEqual(new Date(REF_YEAR, 0, 1))
|
|
54
|
+
expect(result!.end).toEqual(new Date(REF_YEAR, 5, 30))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('parses [H2] as Jul 1 – Dec 31', () => {
|
|
58
|
+
const result = parseDateRangeFromTitle('[H2] Second half', REF_YEAR)
|
|
59
|
+
expect(result).not.toBeNull()
|
|
60
|
+
expect(result!.start).toEqual(new Date(REF_YEAR, 6, 1))
|
|
61
|
+
expect(result!.end).toEqual(new Date(REF_YEAR, 11, 31))
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('quarter — Q1–Q4', () => {
|
|
66
|
+
it('parses [Q1] as Jan–Mar', () => {
|
|
67
|
+
const result = parseDateRangeFromTitle('[Q1] First quarter', REF_YEAR)
|
|
68
|
+
expect(result).not.toBeNull()
|
|
69
|
+
expect(result!.start.getMonth()).toBe(0) // Jan
|
|
70
|
+
expect(result!.end.getMonth()).toBe(2) // Mar
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('parses [Q2] as Apr–Jun', () => {
|
|
74
|
+
const result = parseDateRangeFromTitle('[Q2] Q2 goals', REF_YEAR)
|
|
75
|
+
expect(result).not.toBeNull()
|
|
76
|
+
expect(result!.start.getMonth()).toBe(3)
|
|
77
|
+
expect(result!.end.getMonth()).toBe(5)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('parses [Q3] as Jul–Sep', () => {
|
|
81
|
+
const result = parseDateRangeFromTitle('[Q3] Q3 goals', REF_YEAR)
|
|
82
|
+
expect(result).not.toBeNull()
|
|
83
|
+
expect(result!.start.getMonth()).toBe(6)
|
|
84
|
+
expect(result!.end.getMonth()).toBe(8)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('parses [Q4] as Oct–Dec', () => {
|
|
88
|
+
const result = parseDateRangeFromTitle('[Q4] Q4 goals', REF_YEAR)
|
|
89
|
+
expect(result).not.toBeNull()
|
|
90
|
+
expect(result!.start.getMonth()).toBe(9)
|
|
91
|
+
expect(result!.end.getMonth()).toBe(11)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('month tags', () => {
|
|
96
|
+
it('parses [Jan] as January', () => {
|
|
97
|
+
const result = parseDateRangeFromTitle('[Jan] January sprint', REF_YEAR)
|
|
98
|
+
expect(result).not.toBeNull()
|
|
99
|
+
expect(result!.start.getMonth()).toBe(0)
|
|
100
|
+
expect(result!.end.getMonth()).toBe(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('parses [Jun] as June', () => {
|
|
104
|
+
const result = parseDateRangeFromTitle('[Jun] Mid-year review', REF_YEAR)
|
|
105
|
+
expect(result).not.toBeNull()
|
|
106
|
+
expect(result!.start.getMonth()).toBe(5)
|
|
107
|
+
expect(result!.end.getMonth()).toBe(5)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('parses [Dec] as December', () => {
|
|
111
|
+
const result = parseDateRangeFromTitle('[Dec] Year-end', REF_YEAR)
|
|
112
|
+
expect(result).not.toBeNull()
|
|
113
|
+
expect(result!.start.getMonth()).toBe(11)
|
|
114
|
+
expect(result!.end.getMonth()).toBe(11)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('month start date is the 1st', () => {
|
|
118
|
+
const result = parseDateRangeFromTitle('[Mar] Sprint', REF_YEAR)
|
|
119
|
+
expect(result!.start.getDate()).toBe(1)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('week tags — Wn', () => {
|
|
124
|
+
it('parses [W1] as the first week of the year', () => {
|
|
125
|
+
const result = parseDateRangeFromTitle('[W1] Week 1', REF_YEAR)
|
|
126
|
+
expect(result).not.toBeNull()
|
|
127
|
+
expect(result!.start.getFullYear()).toBe(REF_YEAR)
|
|
128
|
+
// W1 should be 7 days
|
|
129
|
+
const diff = result!.end.getTime() - result!.start.getTime()
|
|
130
|
+
expect(diff).toBe(6 * 24 * 60 * 60 * 1000)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('parses [W10] as the 10th week', () => {
|
|
134
|
+
const result = parseDateRangeFromTitle('[W10] Sprint', REF_YEAR)
|
|
135
|
+
expect(result).not.toBeNull()
|
|
136
|
+
// W10 starts 63 days into the year (0-indexed: (10-1)*7 = 63)
|
|
137
|
+
const expectedStart = new Date(REF_YEAR, 0, 1 + 63)
|
|
138
|
+
expect(result!.start.toDateString()).toBe(expectedStart.toDateString())
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('unknown tag', () => {
|
|
143
|
+
it('returns null for an unrecognised bracket tag', () => {
|
|
144
|
+
expect(parseDateRangeFromTitle('[FOO] Something', REF_YEAR)).toBeNull()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('getHierarchyLevel', () => {
|
|
152
|
+
it('returns 0 for plain titles with no bracket tag', () => {
|
|
153
|
+
expect(getHierarchyLevel('Plain milestone')).toBe(0)
|
|
154
|
+
expect(getHierarchyLevel('')).toBe(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('returns highest level (100+) for year tags', () => {
|
|
158
|
+
expect(getHierarchyLevel('[1yr] Company OKR')).toBe(101)
|
|
159
|
+
expect(getHierarchyLevel('[3yr] Long-term')).toBe(103)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('returns 50 for H1 / H2 tags', () => {
|
|
163
|
+
expect(getHierarchyLevel('[H1] First half')).toBe(50)
|
|
164
|
+
expect(getHierarchyLevel('[H2] Second half')).toBe(50)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('returns 40 for quarter tags', () => {
|
|
168
|
+
expect(getHierarchyLevel('[Q1] Quarter one')).toBe(40)
|
|
169
|
+
expect(getHierarchyLevel('[Q4] Quarter four')).toBe(40)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns 30 for month tags', () => {
|
|
173
|
+
expect(getHierarchyLevel('[Jan] January')).toBe(30)
|
|
174
|
+
expect(getHierarchyLevel('[Dec] December')).toBe(30)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('returns 20 for week tags', () => {
|
|
178
|
+
expect(getHierarchyLevel('[W1] Week 1')).toBe(20)
|
|
179
|
+
expect(getHierarchyLevel('[W52] Week 52')).toBe(20)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('returns 10 for unrecognised bracket tags', () => {
|
|
183
|
+
expect(getHierarchyLevel('[FOO] Something')).toBe(10)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('enforces ordering: year > half > quarter > month > week > custom', () => {
|
|
187
|
+
const levels = [
|
|
188
|
+
getHierarchyLevel('[1yr] Goal'),
|
|
189
|
+
getHierarchyLevel('[H1] Goal'),
|
|
190
|
+
getHierarchyLevel('[Q2] Goal'),
|
|
191
|
+
getHierarchyLevel('[Mar] Goal'),
|
|
192
|
+
getHierarchyLevel('[W5] Goal'),
|
|
193
|
+
getHierarchyLevel('[FOO] Goal'),
|
|
194
|
+
getHierarchyLevel('No tag'),
|
|
195
|
+
]
|
|
196
|
+
// Each subsequent level should be strictly less than the previous
|
|
197
|
+
for (let i = 1; i < levels.length; i++) {
|
|
198
|
+
expect(levels[i]).toBeLessThan(levels[i - 1])
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// progress.ts
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
describe('calculateExpectedProgress', () => {
|
|
208
|
+
it('returns 0 when current date equals start date', () => {
|
|
209
|
+
const start = new Date('2025-01-01')
|
|
210
|
+
const end = new Date('2025-12-31')
|
|
211
|
+
expect(calculateExpectedProgress(start, end, start)).toBe(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('returns 0 when current date is before start date', () => {
|
|
215
|
+
const start = new Date('2025-06-01')
|
|
216
|
+
const end = new Date('2025-12-31')
|
|
217
|
+
const before = new Date('2025-01-01')
|
|
218
|
+
expect(calculateExpectedProgress(start, end, before)).toBe(0)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('returns 100 when current date equals end date', () => {
|
|
222
|
+
const start = new Date('2025-01-01')
|
|
223
|
+
const end = new Date('2025-12-31')
|
|
224
|
+
expect(calculateExpectedProgress(start, end, end)).toBe(100)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('returns 100 when current date is after end date', () => {
|
|
228
|
+
const start = new Date('2025-01-01')
|
|
229
|
+
const end = new Date('2025-06-01')
|
|
230
|
+
const after = new Date('2025-12-31')
|
|
231
|
+
expect(calculateExpectedProgress(start, end, after)).toBe(100)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('returns ~50 when current date is halfway through', () => {
|
|
235
|
+
const start = new Date('2025-01-01')
|
|
236
|
+
const end = new Date('2025-01-11') // 10 days
|
|
237
|
+
const mid = new Date('2025-01-06') // 5 days elapsed
|
|
238
|
+
const result = calculateExpectedProgress(start, end, mid)
|
|
239
|
+
// 5/10 = 50%
|
|
240
|
+
expect(result).toBe(50)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns a value between 0 and 100 exclusive for a mid-point', () => {
|
|
244
|
+
const start = new Date('2025-01-01')
|
|
245
|
+
const end = new Date('2025-12-31')
|
|
246
|
+
const mid = new Date('2025-07-01')
|
|
247
|
+
const result = calculateExpectedProgress(start, end, mid)
|
|
248
|
+
expect(result).toBeGreaterThan(0)
|
|
249
|
+
expect(result).toBeLessThan(100)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('rounds to an integer', () => {
|
|
253
|
+
const start = new Date('2025-01-01')
|
|
254
|
+
const end = new Date('2025-01-04') // 3 days
|
|
255
|
+
const current = new Date('2025-01-02') // 1 day elapsed → 33.33%
|
|
256
|
+
const result = calculateExpectedProgress(start, end, current)
|
|
257
|
+
expect(Number.isInteger(result)).toBe(true)
|
|
258
|
+
expect(result).toBe(33)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
describe('calculateHealthStatus', () => {
|
|
265
|
+
it('returns "blocked" when hasBlockers is true, regardless of progress', () => {
|
|
266
|
+
const result = calculateHealthStatus(80, 30, true)
|
|
267
|
+
expect(result.status).toBe('blocked')
|
|
268
|
+
expect(result.reason).toBeDefined()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('returns "not_started" when progress is 0 and expected > 0', () => {
|
|
272
|
+
const result = calculateHealthStatus(0, 25)
|
|
273
|
+
expect(result.status).toBe('not_started')
|
|
274
|
+
expect(result.reason).toBeDefined()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('returns "on_track" when progress is 0 and expected is also 0', () => {
|
|
278
|
+
const result = calculateHealthStatus(0, 0)
|
|
279
|
+
expect(result.status).toBe('on_track')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('returns "at_risk" when progress is more than 10 points behind expected', () => {
|
|
283
|
+
const result = calculateHealthStatus(30, 50) // 20 points behind
|
|
284
|
+
expect(result.status).toBe('at_risk')
|
|
285
|
+
expect(result.reason).toBeDefined()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('returns "on_track" when actual progress equals expected', () => {
|
|
289
|
+
const result = calculateHealthStatus(60, 60)
|
|
290
|
+
expect(result.status).toBe('on_track')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('returns "on_track" when actual progress is ahead of expected', () => {
|
|
294
|
+
const result = calculateHealthStatus(70, 50)
|
|
295
|
+
expect(result.status).toBe('on_track')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('returns "on_track" when actual is exactly 10 points behind (boundary)', () => {
|
|
299
|
+
// diff = 40 - 50 = -10, which is NOT < -10, so on_track
|
|
300
|
+
const result = calculateHealthStatus(40, 50)
|
|
301
|
+
expect(result.status).toBe('on_track')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('returns "at_risk" when actual is 11 points behind (just past boundary)', () => {
|
|
305
|
+
const result = calculateHealthStatus(39, 50)
|
|
306
|
+
expect(result.status).toBe('at_risk')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('blocked takes precedence over not_started', () => {
|
|
310
|
+
const result = calculateHealthStatus(0, 50, true)
|
|
311
|
+
expect(result.status).toBe('blocked')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
describe('getHealthColor', () => {
|
|
318
|
+
it('returns green for on_track', () => {
|
|
319
|
+
expect(getHealthColor('on_track')).toBe('#22c55e')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('returns yellow for at_risk', () => {
|
|
323
|
+
expect(getHealthColor('at_risk')).toBe('#eab308')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('returns red for blocked', () => {
|
|
327
|
+
expect(getHealthColor('blocked')).toBe('#ef4444')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('returns gray for not_started', () => {
|
|
331
|
+
expect(getHealthColor('not_started')).toBe('#9ca3af')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('returns a non-empty CSS color string for every valid status', () => {
|
|
335
|
+
const statuses = ['on_track', 'at_risk', 'blocked', 'not_started'] as const
|
|
336
|
+
for (const s of statuses) {
|
|
337
|
+
const color = getHealthColor(s)
|
|
338
|
+
expect(color).toMatch(/^#[0-9a-f]{6}$/i)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
})
|