mgv-backoffice 1.36.0 → 1.36.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1794 -1719
- package/dist/components/BaseBadge.vue.d.ts +1 -11
- package/dist/components/BaseDropdown.vue.d.ts +1 -1
- package/dist/ui-lib.js +942 -925
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/specForm.d.ts +1 -1
- package/package.json +54 -54
- package/src/components/BaseAlert.vue +104 -104
- package/src/components/BaseBadge.vue +12 -2
- package/src/components/BaseBarDistribution.test.ts +51 -51
- package/src/components/BaseBarDistribution.vue +73 -73
- package/src/components/BaseChipButton.vue +42 -42
- package/src/components/BaseCodeBlock.test.ts +70 -70
- package/src/components/BaseCodeBlock.vue +74 -74
- package/src/components/BaseCollapsibleSection.vue +113 -113
- package/src/components/BaseConfirmModal.vue +114 -114
- package/src/components/BaseCredentialsForm.test.ts +108 -108
- package/src/components/BaseCredentialsForm.vue +241 -241
- package/src/components/BaseDropdown.test.ts +87 -87
- package/src/components/BaseDropdown.vue +172 -172
- package/src/components/BaseEntityPickerModal.vue +330 -330
- package/src/components/BaseFileDropzone.vue +94 -94
- package/src/components/BaseFilterChip.test.ts +44 -44
- package/src/components/BaseFilterChip.vue +67 -67
- package/src/components/BaseModalShell.vue +209 -209
- package/src/components/BasePageHeader.vue +120 -120
- package/src/components/BasePillPickerModal.test.ts +95 -95
- package/src/components/BaseRemoveButton.vue +22 -22
- package/src/components/BaseSpecFields.test.ts +39 -0
- package/src/components/BaseSpecFields.vue +27 -3
- package/src/components/BaseStatBreakdown.test.ts +56 -56
- package/src/components/BaseStatBreakdown.vue +49 -49
- package/src/components/BaseStatusPill.vue +54 -54
- package/src/components/BaseTextInputModal.vue +140 -140
- package/src/components/BaseToolbarButton.test.ts +48 -48
- package/src/components/BaseToolbarButton.vue +94 -94
- package/src/composables/useFieldClasses.test.ts +55 -55
- package/src/composables/useFieldClasses.ts +56 -56
- package/src/composables/usePolling.test.ts +112 -112
- package/src/composables/usePolling.ts +102 -102
- package/src/composables/useQueryParamSync.test.ts +56 -56
- package/src/composables/useQueryParamSync.ts +42 -42
- package/src/composables/useThemeClasses.test.ts +34 -34
- package/src/composables/useThemeClasses.ts +137 -137
- package/src/index.ts +138 -138
- package/src/style.css +32 -32
- package/src/types/distribution.ts +7 -7
- package/src/types/pillPicker.ts +14 -14
- package/src/types/specField.ts +30 -30
- package/src/types/statBreakdown.ts +6 -6
- package/src/utils/format.test.ts +264 -264
- package/src/utils/format.ts +216 -216
- package/src/utils/httpColors.test.ts +100 -100
- package/src/utils/httpColors.ts +99 -99
- package/src/utils/kvRows.test.ts +47 -47
- package/src/utils/kvRows.ts +34 -34
- package/src/utils/specForm.test.ts +64 -53
- package/src/utils/specForm.ts +49 -48
- package/src/utils/validate.test.ts +82 -82
- package/src/utils/validate.ts +76 -76
package/src/utils/format.test.ts
CHANGED
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
fmtNumber,
|
|
4
|
-
fmtDate,
|
|
5
|
-
fmtDateTime,
|
|
6
|
-
fmtDateTimeMs,
|
|
7
|
-
fmtDateShort,
|
|
8
|
-
fmtPrice,
|
|
9
|
-
fmtPct,
|
|
10
|
-
fmtUsd,
|
|
11
|
-
fmtDuration,
|
|
12
|
-
formatJson,
|
|
13
|
-
stringifyValue,
|
|
14
|
-
} from './format'
|
|
15
|
-
|
|
16
|
-
describe('fmtDateTimeMs', () => {
|
|
17
|
-
it('keeps the millisecond fraction so same-second rows stay distinguishable', () => {
|
|
18
|
-
const out = fmtDateTimeMs('2026-06-08T14:30:51.123Z')
|
|
19
|
-
expect(out).toMatch(/51[.,:]?\d*123|123/) // ms fraction present, locale separators vary
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('accepts epoch millis', () => {
|
|
23
|
-
expect(fmtDateTimeMs(0)).toMatch(/19?70/)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('falls back to the raw value for unparseable input', () => {
|
|
27
|
-
expect(fmtDateTimeMs('not-a-date')).toBe('Invalid Date')
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const EM_DASH = '—'
|
|
32
|
-
|
|
33
|
-
// Fraction digits are locale-stable even when the grouping/decimal
|
|
34
|
-
// separator is not, so assert on the count rather than the exact string.
|
|
35
|
-
function fractionLen(s: string): number {
|
|
36
|
-
const m = s.match(/[.,](\d+)$/)
|
|
37
|
-
return m ? m[1].length : 0
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('fmtNumber', () => {
|
|
41
|
-
it('renders an em-dash for null/undefined', () => {
|
|
42
|
-
expect(fmtNumber(null)).toBe(EM_DASH)
|
|
43
|
-
expect(fmtNumber(undefined)).toBe(EM_DASH)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('renders an em-dash for empty/whitespace strings', () => {
|
|
47
|
-
expect(fmtNumber('')).toBe(EM_DASH)
|
|
48
|
-
expect(fmtNumber(' ')).toBe(EM_DASH)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('renders an em-dash for non-numeric strings and non-finite values', () => {
|
|
52
|
-
expect(fmtNumber('abc')).toBe(EM_DASH)
|
|
53
|
-
expect(fmtNumber(Infinity)).toBe(EM_DASH)
|
|
54
|
-
expect(fmtNumber(NaN)).toBe(EM_DASH)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('formats numbers with the default of 4 fraction digits', () => {
|
|
58
|
-
expect(fractionLen(fmtNumber(1))).toBe(4)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('honours the digits argument', () => {
|
|
62
|
-
expect(fractionLen(fmtNumber(1.5, 2))).toBe(2)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('accepts numeric strings', () => {
|
|
66
|
-
expect(fractionLen(fmtNumber('3.14159', 2))).toBe(2)
|
|
67
|
-
expect(fmtNumber('0', 2)).toMatch(/^0[.,]00$/)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('fmtDate', () => {
|
|
72
|
-
it('renders an em-dash for empty input', () => {
|
|
73
|
-
expect(fmtDate(null)).toBe(EM_DASH)
|
|
74
|
-
expect(fmtDate(undefined)).toBe(EM_DASH)
|
|
75
|
-
expect(fmtDate('')).toBe(EM_DASH)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('falls back to the raw value on parse failure', () => {
|
|
79
|
-
expect(fmtDate('not a date')).toBe('not a date')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('formats a valid ISO string', () => {
|
|
83
|
-
const out = fmtDate('2026-05-30T12:00:00Z')
|
|
84
|
-
expect(out).not.toBe(EM_DASH)
|
|
85
|
-
expect(out).not.toBe('Invalid Date')
|
|
86
|
-
expect(out.length).toBeGreaterThan(0)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('formats an epoch value', () => {
|
|
90
|
-
expect(fmtDate(0)).not.toBe(EM_DASH)
|
|
91
|
-
expect(fmtDate(0)).not.toBe('Invalid Date')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('fmtDateTime / fmtDateShort', () => {
|
|
96
|
-
const ms = Date.UTC(2026, 4, 30, 12, 0)
|
|
97
|
-
|
|
98
|
-
it('returns a non-empty label without "Invalid"', () => {
|
|
99
|
-
expect(fmtDateTime(ms)).not.toContain('Invalid')
|
|
100
|
-
expect(fmtDateTime(ms).length).toBeGreaterThan(0)
|
|
101
|
-
expect(fmtDateShort(ms)).not.toContain('Invalid')
|
|
102
|
-
expect(fmtDateShort(ms).length).toBeGreaterThan(0)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('fmtPrice', () => {
|
|
107
|
-
it('returns 0.00 for zero', () => {
|
|
108
|
-
expect(fmtPrice(0)).toBe('0.00')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('returns NaN for non-finite input', () => {
|
|
112
|
-
expect(fmtPrice(NaN)).toBe('NaN')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('scales precision up for sub-cent magnitudes', () => {
|
|
116
|
-
expect(fractionLen(fmtPrice(0.00005))).toBe(8)
|
|
117
|
-
expect(fractionLen(fmtPrice(0.005))).toBe(6)
|
|
118
|
-
expect(fractionLen(fmtPrice(0.5))).toBe(4)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('uses 3 digits for magnitudes under 100', () => {
|
|
122
|
-
expect(fractionLen(fmtPrice(12.5))).toBe(3)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('uses 2 digits for magnitudes of 100 or more', () => {
|
|
126
|
-
expect(fractionLen(fmtPrice(150))).toBe(2)
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('fmtPct', () => {
|
|
131
|
-
it('prefixes a sign', () => {
|
|
132
|
-
expect(fmtPct(2.5)).toBe('+2.50%')
|
|
133
|
-
expect(fmtPct(-1)).toBe('-1.00%')
|
|
134
|
-
expect(fmtPct(0)).toBe('+0.00%')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('honours the digits argument', () => {
|
|
138
|
-
expect(fmtPct(3.14159, 3)).toBe('+3.142%')
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('fmtUsd', () => {
|
|
143
|
-
it('formats a signed USD amount', () => {
|
|
144
|
-
expect(fmtUsd(5)).toBe('+$5.00')
|
|
145
|
-
expect(fmtUsd(-3.5)).toBe('-$3.50')
|
|
146
|
-
expect(fmtUsd(0)).toBe('+$0.00')
|
|
147
|
-
})
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
describe('fmtDuration', () => {
|
|
151
|
-
it('renders an em-dash for null/undefined/non-finite input', () => {
|
|
152
|
-
expect(fmtDuration(null)).toBe(EM_DASH)
|
|
153
|
-
expect(fmtDuration(undefined)).toBe(EM_DASH)
|
|
154
|
-
expect(fmtDuration(NaN)).toBe(EM_DASH)
|
|
155
|
-
expect(fmtDuration(Infinity)).toBe(EM_DASH)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('drops zero-value leading units', () => {
|
|
159
|
-
expect(fmtDuration(12 * 60_000)).toBe('12m')
|
|
160
|
-
expect(fmtDuration((5 * 60 + 12) * 60_000)).toBe('5h 12m')
|
|
161
|
-
expect(fmtDuration((3 * 1440 + 5 * 60 + 12) * 60_000)).toBe('3d 5h')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('caps the rendered units at maxUnits', () => {
|
|
165
|
-
const ms = (2 * 1440 + 5 * 60 + 13) * 60_000
|
|
166
|
-
expect(fmtDuration(ms, 3)).toBe('2d 5h 13m')
|
|
167
|
-
expect(fmtDuration(ms, 2)).toBe('2d 5h')
|
|
168
|
-
expect(fmtDuration(ms, 1)).toBe('2d')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('keeps the hours unit when days are present even if hours are zero', () => {
|
|
172
|
-
expect(fmtDuration(2 * 1440 * 60_000, 3)).toBe('2d 0h 0m')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('clamps negative durations to zero', () => {
|
|
176
|
-
expect(fmtDuration(-5000)).toBe('0m')
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('renders sub-minute durations as 0m', () => {
|
|
180
|
-
expect(fmtDuration(59_000)).toBe('0m')
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
import { fmtCalendarDate, fmtCalendarDateTime, fmtMsAsSeconds, fmtBytes } from './format'
|
|
185
|
-
|
|
186
|
-
describe('fmtCalendarDate', () => {
|
|
187
|
-
it('renders a "Mon D, YYYY" en-US label', () => {
|
|
188
|
-
expect(fmtCalendarDate('2026-01-05T10:30:00Z')).toMatch(/^Jan \d{1,2}, 2026$/)
|
|
189
|
-
})
|
|
190
|
-
it('renders an em-dash for empty input', () => {
|
|
191
|
-
expect(fmtCalendarDate(null)).toBe(EM_DASH)
|
|
192
|
-
expect(fmtCalendarDate('')).toBe(EM_DASH)
|
|
193
|
-
})
|
|
194
|
-
it('falls back to the raw value for unparseable input', () => {
|
|
195
|
-
expect(fmtCalendarDate('not-a-date')).toBe('not-a-date')
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
describe('fmtCalendarDateTime', () => {
|
|
200
|
-
it('includes the time of day', () => {
|
|
201
|
-
expect(fmtCalendarDateTime('2026-01-05T10:30:00Z')).toMatch(/2026.*\d{1,2}:\d{2}/)
|
|
202
|
-
})
|
|
203
|
-
it('renders an em-dash for empty input', () => {
|
|
204
|
-
expect(fmtCalendarDateTime(undefined)).toBe(EM_DASH)
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
describe('fmtMsAsSeconds', () => {
|
|
209
|
-
it('renders 2 decimals at >= 1s', () => {
|
|
210
|
-
expect(fmtMsAsSeconds(1500)).toBe('= 1.50 s')
|
|
211
|
-
})
|
|
212
|
-
it('renders 3 decimals below 1s', () => {
|
|
213
|
-
expect(fmtMsAsSeconds(250)).toBe('= 0.250 s')
|
|
214
|
-
})
|
|
215
|
-
it('returns empty for zero / negative / non-finite input', () => {
|
|
216
|
-
expect(fmtMsAsSeconds(0)).toBe('')
|
|
217
|
-
expect(fmtMsAsSeconds(-5)).toBe('')
|
|
218
|
-
expect(fmtMsAsSeconds(NaN)).toBe('')
|
|
219
|
-
expect(fmtMsAsSeconds(null)).toBe('')
|
|
220
|
-
})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
describe('fmtBytes', () => {
|
|
224
|
-
it('renders bytes below 1 KB', () => {
|
|
225
|
-
expect(fmtBytes(512)).toBe('512 B')
|
|
226
|
-
})
|
|
227
|
-
it('renders KB with one decimal', () => {
|
|
228
|
-
expect(fmtBytes(1536)).toBe('1.5 KB')
|
|
229
|
-
})
|
|
230
|
-
it('renders MB with one decimal', () => {
|
|
231
|
-
expect(fmtBytes(2 * 1024 * 1024)).toBe('2.0 MB')
|
|
232
|
-
})
|
|
233
|
-
it('returns empty for zero / falsy input', () => {
|
|
234
|
-
expect(fmtBytes(0)).toBe('')
|
|
235
|
-
expect(fmtBytes(null)).toBe('')
|
|
236
|
-
})
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
describe('formatJson', () => {
|
|
240
|
-
it('pretty-prints parseable JSON with 2-space indentation', () => {
|
|
241
|
-
expect(formatJson('{"a":1,"b":[2,3]}')).toBe('{\n "a": 1,\n "b": [\n 2,\n 3\n ]\n}')
|
|
242
|
-
})
|
|
243
|
-
it('returns non-JSON input verbatim', () => {
|
|
244
|
-
expect(formatJson('plain text')).toBe('plain text')
|
|
245
|
-
expect(formatJson('<xml/>')).toBe('<xml/>')
|
|
246
|
-
expect(formatJson('')).toBe('')
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
describe('stringifyValue', () => {
|
|
251
|
-
it('passes strings through and blanks null/undefined', () => {
|
|
252
|
-
expect(stringifyValue('abc')).toBe('abc')
|
|
253
|
-
expect(stringifyValue(null)).toBe('')
|
|
254
|
-
expect(stringifyValue(undefined)).toBe('')
|
|
255
|
-
})
|
|
256
|
-
it('JSON-serializes objects, arrays and numbers', () => {
|
|
257
|
-
expect(stringifyValue({ a: 1 })).toBe('{"a":1}')
|
|
258
|
-
expect(stringifyValue([1, 2])).toBe('[1,2]')
|
|
259
|
-
expect(stringifyValue(42)).toBe('42')
|
|
260
|
-
})
|
|
261
|
-
it('falls back to String() when JSON.stringify throws', () => {
|
|
262
|
-
expect(stringifyValue(1n)).toBe('1')
|
|
263
|
-
})
|
|
264
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
fmtNumber,
|
|
4
|
+
fmtDate,
|
|
5
|
+
fmtDateTime,
|
|
6
|
+
fmtDateTimeMs,
|
|
7
|
+
fmtDateShort,
|
|
8
|
+
fmtPrice,
|
|
9
|
+
fmtPct,
|
|
10
|
+
fmtUsd,
|
|
11
|
+
fmtDuration,
|
|
12
|
+
formatJson,
|
|
13
|
+
stringifyValue,
|
|
14
|
+
} from './format'
|
|
15
|
+
|
|
16
|
+
describe('fmtDateTimeMs', () => {
|
|
17
|
+
it('keeps the millisecond fraction so same-second rows stay distinguishable', () => {
|
|
18
|
+
const out = fmtDateTimeMs('2026-06-08T14:30:51.123Z')
|
|
19
|
+
expect(out).toMatch(/51[.,:]?\d*123|123/) // ms fraction present, locale separators vary
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('accepts epoch millis', () => {
|
|
23
|
+
expect(fmtDateTimeMs(0)).toMatch(/19?70/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('falls back to the raw value for unparseable input', () => {
|
|
27
|
+
expect(fmtDateTimeMs('not-a-date')).toBe('Invalid Date')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const EM_DASH = '—'
|
|
32
|
+
|
|
33
|
+
// Fraction digits are locale-stable even when the grouping/decimal
|
|
34
|
+
// separator is not, so assert on the count rather than the exact string.
|
|
35
|
+
function fractionLen(s: string): number {
|
|
36
|
+
const m = s.match(/[.,](\d+)$/)
|
|
37
|
+
return m ? m[1].length : 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('fmtNumber', () => {
|
|
41
|
+
it('renders an em-dash for null/undefined', () => {
|
|
42
|
+
expect(fmtNumber(null)).toBe(EM_DASH)
|
|
43
|
+
expect(fmtNumber(undefined)).toBe(EM_DASH)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders an em-dash for empty/whitespace strings', () => {
|
|
47
|
+
expect(fmtNumber('')).toBe(EM_DASH)
|
|
48
|
+
expect(fmtNumber(' ')).toBe(EM_DASH)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders an em-dash for non-numeric strings and non-finite values', () => {
|
|
52
|
+
expect(fmtNumber('abc')).toBe(EM_DASH)
|
|
53
|
+
expect(fmtNumber(Infinity)).toBe(EM_DASH)
|
|
54
|
+
expect(fmtNumber(NaN)).toBe(EM_DASH)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('formats numbers with the default of 4 fraction digits', () => {
|
|
58
|
+
expect(fractionLen(fmtNumber(1))).toBe(4)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('honours the digits argument', () => {
|
|
62
|
+
expect(fractionLen(fmtNumber(1.5, 2))).toBe(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('accepts numeric strings', () => {
|
|
66
|
+
expect(fractionLen(fmtNumber('3.14159', 2))).toBe(2)
|
|
67
|
+
expect(fmtNumber('0', 2)).toMatch(/^0[.,]00$/)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('fmtDate', () => {
|
|
72
|
+
it('renders an em-dash for empty input', () => {
|
|
73
|
+
expect(fmtDate(null)).toBe(EM_DASH)
|
|
74
|
+
expect(fmtDate(undefined)).toBe(EM_DASH)
|
|
75
|
+
expect(fmtDate('')).toBe(EM_DASH)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('falls back to the raw value on parse failure', () => {
|
|
79
|
+
expect(fmtDate('not a date')).toBe('not a date')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('formats a valid ISO string', () => {
|
|
83
|
+
const out = fmtDate('2026-05-30T12:00:00Z')
|
|
84
|
+
expect(out).not.toBe(EM_DASH)
|
|
85
|
+
expect(out).not.toBe('Invalid Date')
|
|
86
|
+
expect(out.length).toBeGreaterThan(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('formats an epoch value', () => {
|
|
90
|
+
expect(fmtDate(0)).not.toBe(EM_DASH)
|
|
91
|
+
expect(fmtDate(0)).not.toBe('Invalid Date')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('fmtDateTime / fmtDateShort', () => {
|
|
96
|
+
const ms = Date.UTC(2026, 4, 30, 12, 0)
|
|
97
|
+
|
|
98
|
+
it('returns a non-empty label without "Invalid"', () => {
|
|
99
|
+
expect(fmtDateTime(ms)).not.toContain('Invalid')
|
|
100
|
+
expect(fmtDateTime(ms).length).toBeGreaterThan(0)
|
|
101
|
+
expect(fmtDateShort(ms)).not.toContain('Invalid')
|
|
102
|
+
expect(fmtDateShort(ms).length).toBeGreaterThan(0)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('fmtPrice', () => {
|
|
107
|
+
it('returns 0.00 for zero', () => {
|
|
108
|
+
expect(fmtPrice(0)).toBe('0.00')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns NaN for non-finite input', () => {
|
|
112
|
+
expect(fmtPrice(NaN)).toBe('NaN')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('scales precision up for sub-cent magnitudes', () => {
|
|
116
|
+
expect(fractionLen(fmtPrice(0.00005))).toBe(8)
|
|
117
|
+
expect(fractionLen(fmtPrice(0.005))).toBe(6)
|
|
118
|
+
expect(fractionLen(fmtPrice(0.5))).toBe(4)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('uses 3 digits for magnitudes under 100', () => {
|
|
122
|
+
expect(fractionLen(fmtPrice(12.5))).toBe(3)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('uses 2 digits for magnitudes of 100 or more', () => {
|
|
126
|
+
expect(fractionLen(fmtPrice(150))).toBe(2)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('fmtPct', () => {
|
|
131
|
+
it('prefixes a sign', () => {
|
|
132
|
+
expect(fmtPct(2.5)).toBe('+2.50%')
|
|
133
|
+
expect(fmtPct(-1)).toBe('-1.00%')
|
|
134
|
+
expect(fmtPct(0)).toBe('+0.00%')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('honours the digits argument', () => {
|
|
138
|
+
expect(fmtPct(3.14159, 3)).toBe('+3.142%')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('fmtUsd', () => {
|
|
143
|
+
it('formats a signed USD amount', () => {
|
|
144
|
+
expect(fmtUsd(5)).toBe('+$5.00')
|
|
145
|
+
expect(fmtUsd(-3.5)).toBe('-$3.50')
|
|
146
|
+
expect(fmtUsd(0)).toBe('+$0.00')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('fmtDuration', () => {
|
|
151
|
+
it('renders an em-dash for null/undefined/non-finite input', () => {
|
|
152
|
+
expect(fmtDuration(null)).toBe(EM_DASH)
|
|
153
|
+
expect(fmtDuration(undefined)).toBe(EM_DASH)
|
|
154
|
+
expect(fmtDuration(NaN)).toBe(EM_DASH)
|
|
155
|
+
expect(fmtDuration(Infinity)).toBe(EM_DASH)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('drops zero-value leading units', () => {
|
|
159
|
+
expect(fmtDuration(12 * 60_000)).toBe('12m')
|
|
160
|
+
expect(fmtDuration((5 * 60 + 12) * 60_000)).toBe('5h 12m')
|
|
161
|
+
expect(fmtDuration((3 * 1440 + 5 * 60 + 12) * 60_000)).toBe('3d 5h')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('caps the rendered units at maxUnits', () => {
|
|
165
|
+
const ms = (2 * 1440 + 5 * 60 + 13) * 60_000
|
|
166
|
+
expect(fmtDuration(ms, 3)).toBe('2d 5h 13m')
|
|
167
|
+
expect(fmtDuration(ms, 2)).toBe('2d 5h')
|
|
168
|
+
expect(fmtDuration(ms, 1)).toBe('2d')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('keeps the hours unit when days are present even if hours are zero', () => {
|
|
172
|
+
expect(fmtDuration(2 * 1440 * 60_000, 3)).toBe('2d 0h 0m')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('clamps negative durations to zero', () => {
|
|
176
|
+
expect(fmtDuration(-5000)).toBe('0m')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('renders sub-minute durations as 0m', () => {
|
|
180
|
+
expect(fmtDuration(59_000)).toBe('0m')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
import { fmtCalendarDate, fmtCalendarDateTime, fmtMsAsSeconds, fmtBytes } from './format'
|
|
185
|
+
|
|
186
|
+
describe('fmtCalendarDate', () => {
|
|
187
|
+
it('renders a "Mon D, YYYY" en-US label', () => {
|
|
188
|
+
expect(fmtCalendarDate('2026-01-05T10:30:00Z')).toMatch(/^Jan \d{1,2}, 2026$/)
|
|
189
|
+
})
|
|
190
|
+
it('renders an em-dash for empty input', () => {
|
|
191
|
+
expect(fmtCalendarDate(null)).toBe(EM_DASH)
|
|
192
|
+
expect(fmtCalendarDate('')).toBe(EM_DASH)
|
|
193
|
+
})
|
|
194
|
+
it('falls back to the raw value for unparseable input', () => {
|
|
195
|
+
expect(fmtCalendarDate('not-a-date')).toBe('not-a-date')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('fmtCalendarDateTime', () => {
|
|
200
|
+
it('includes the time of day', () => {
|
|
201
|
+
expect(fmtCalendarDateTime('2026-01-05T10:30:00Z')).toMatch(/2026.*\d{1,2}:\d{2}/)
|
|
202
|
+
})
|
|
203
|
+
it('renders an em-dash for empty input', () => {
|
|
204
|
+
expect(fmtCalendarDateTime(undefined)).toBe(EM_DASH)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('fmtMsAsSeconds', () => {
|
|
209
|
+
it('renders 2 decimals at >= 1s', () => {
|
|
210
|
+
expect(fmtMsAsSeconds(1500)).toBe('= 1.50 s')
|
|
211
|
+
})
|
|
212
|
+
it('renders 3 decimals below 1s', () => {
|
|
213
|
+
expect(fmtMsAsSeconds(250)).toBe('= 0.250 s')
|
|
214
|
+
})
|
|
215
|
+
it('returns empty for zero / negative / non-finite input', () => {
|
|
216
|
+
expect(fmtMsAsSeconds(0)).toBe('')
|
|
217
|
+
expect(fmtMsAsSeconds(-5)).toBe('')
|
|
218
|
+
expect(fmtMsAsSeconds(NaN)).toBe('')
|
|
219
|
+
expect(fmtMsAsSeconds(null)).toBe('')
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('fmtBytes', () => {
|
|
224
|
+
it('renders bytes below 1 KB', () => {
|
|
225
|
+
expect(fmtBytes(512)).toBe('512 B')
|
|
226
|
+
})
|
|
227
|
+
it('renders KB with one decimal', () => {
|
|
228
|
+
expect(fmtBytes(1536)).toBe('1.5 KB')
|
|
229
|
+
})
|
|
230
|
+
it('renders MB with one decimal', () => {
|
|
231
|
+
expect(fmtBytes(2 * 1024 * 1024)).toBe('2.0 MB')
|
|
232
|
+
})
|
|
233
|
+
it('returns empty for zero / falsy input', () => {
|
|
234
|
+
expect(fmtBytes(0)).toBe('')
|
|
235
|
+
expect(fmtBytes(null)).toBe('')
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('formatJson', () => {
|
|
240
|
+
it('pretty-prints parseable JSON with 2-space indentation', () => {
|
|
241
|
+
expect(formatJson('{"a":1,"b":[2,3]}')).toBe('{\n "a": 1,\n "b": [\n 2,\n 3\n ]\n}')
|
|
242
|
+
})
|
|
243
|
+
it('returns non-JSON input verbatim', () => {
|
|
244
|
+
expect(formatJson('plain text')).toBe('plain text')
|
|
245
|
+
expect(formatJson('<xml/>')).toBe('<xml/>')
|
|
246
|
+
expect(formatJson('')).toBe('')
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('stringifyValue', () => {
|
|
251
|
+
it('passes strings through and blanks null/undefined', () => {
|
|
252
|
+
expect(stringifyValue('abc')).toBe('abc')
|
|
253
|
+
expect(stringifyValue(null)).toBe('')
|
|
254
|
+
expect(stringifyValue(undefined)).toBe('')
|
|
255
|
+
})
|
|
256
|
+
it('JSON-serializes objects, arrays and numbers', () => {
|
|
257
|
+
expect(stringifyValue({ a: 1 })).toBe('{"a":1}')
|
|
258
|
+
expect(stringifyValue([1, 2])).toBe('[1,2]')
|
|
259
|
+
expect(stringifyValue(42)).toBe('42')
|
|
260
|
+
})
|
|
261
|
+
it('falls back to String() when JSON.stringify throws', () => {
|
|
262
|
+
expect(stringifyValue(1n)).toBe('1')
|
|
263
|
+
})
|
|
264
|
+
})
|