@tuturuuu/utils 0.0.3 → 0.6.0

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.
Files changed (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,408 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ calculateDuration,
4
+ compareTimetz,
5
+ formatTimezoneOffset,
6
+ getDateRange,
7
+ getDateRangeOptions,
8
+ getDateRangeUnits,
9
+ maxTimetz,
10
+ minTimetz,
11
+ parseTimezoneOffset,
12
+ timetzToHour,
13
+ timetzToTime,
14
+ } from '../date-helper';
15
+
16
+ describe('Date Helper', () => {
17
+ describe('timetzToTime', () => {
18
+ beforeEach(() => {
19
+ vi.useFakeTimers();
20
+ // Set system time to a fixed date to control timezone offset
21
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.useRealTimers();
26
+ });
27
+
28
+ it('converts timetz to local time format', () => {
29
+ // This will depend on the user's timezone offset
30
+ const result = timetzToTime('10:00:00+00');
31
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
32
+ });
33
+
34
+ it('handles negative offsets', () => {
35
+ const result = timetzToTime('10:00:00-05');
36
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
37
+ });
38
+
39
+ it('handles positive offsets', () => {
40
+ const result = timetzToTime('10:00:00+07');
41
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
42
+ });
43
+
44
+ it('handles times with minutes', () => {
45
+ const result = timetzToTime('14:30:00+00');
46
+ expect(result).toMatch(/^\d{2}:\d{2}$/);
47
+ });
48
+ });
49
+
50
+ describe('timetzToHour', () => {
51
+ beforeEach(() => {
52
+ vi.useFakeTimers();
53
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
54
+ });
55
+
56
+ afterEach(() => {
57
+ vi.useRealTimers();
58
+ });
59
+
60
+ it('returns undefined for undefined input', () => {
61
+ expect(timetzToHour(undefined)).toBeUndefined();
62
+ });
63
+
64
+ it('extracts hour from timetz string', () => {
65
+ const result = timetzToHour('10:30:00+00');
66
+ expect(typeof result).toBe('number');
67
+ expect(result).toBeGreaterThanOrEqual(0);
68
+ expect(result).toBeLessThan(24);
69
+ });
70
+ });
71
+
72
+ describe('compareTimetz', () => {
73
+ beforeEach(() => {
74
+ vi.useFakeTimers();
75
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
76
+ });
77
+
78
+ afterEach(() => {
79
+ vi.useRealTimers();
80
+ });
81
+
82
+ it('returns negative when first time is earlier', () => {
83
+ const result = compareTimetz('08:00:00+00', '10:00:00+00');
84
+ expect(result).toBeLessThan(0);
85
+ });
86
+
87
+ it('returns positive when first time is later', () => {
88
+ const result = compareTimetz('14:00:00+00', '10:00:00+00');
89
+ expect(result).toBeGreaterThan(0);
90
+ });
91
+
92
+ it('returns zero for same times', () => {
93
+ const result = compareTimetz('10:00:00+00', '10:00:00+00');
94
+ expect(result).toBe(0);
95
+ });
96
+ });
97
+
98
+ describe('minTimetz', () => {
99
+ beforeEach(() => {
100
+ vi.useFakeTimers();
101
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
102
+ });
103
+
104
+ afterEach(() => {
105
+ vi.useRealTimers();
106
+ });
107
+
108
+ it('returns earlier time', () => {
109
+ const result = minTimetz('08:00:00+00', '10:00:00+00');
110
+ expect(result).toBe('08:00:00+00');
111
+ });
112
+
113
+ it('returns first when times are equal', () => {
114
+ const result = minTimetz('10:00:00+00', '10:00:00+00');
115
+ expect(result).toBe('10:00:00+00');
116
+ });
117
+ });
118
+
119
+ describe('maxTimetz', () => {
120
+ beforeEach(() => {
121
+ vi.useFakeTimers();
122
+ vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
123
+ });
124
+
125
+ afterEach(() => {
126
+ vi.useRealTimers();
127
+ });
128
+
129
+ it('returns later time', () => {
130
+ const result = maxTimetz('08:00:00+00', '10:00:00+00');
131
+ expect(result).toBe('10:00:00+00');
132
+ });
133
+
134
+ it('returns first when times are equal', () => {
135
+ const result = maxTimetz('10:00:00+00', '10:00:00+00');
136
+ expect(result).toBe('10:00:00+00');
137
+ });
138
+ });
139
+
140
+ describe('parseTimezoneOffset', () => {
141
+ it('returns empty string for empty input', () => {
142
+ expect(parseTimezoneOffset('')).toBe('');
143
+ });
144
+
145
+ it('parses positive HH:MM offset format', () => {
146
+ expect(parseTimezoneOffset('11:00:00+07:00')).toBe('+07:00');
147
+ });
148
+
149
+ it('parses negative HH:MM offset format', () => {
150
+ expect(parseTimezoneOffset('14:30:00-05:30')).toBe('-05:30');
151
+ });
152
+
153
+ it('parses positive decimal offset format', () => {
154
+ expect(parseTimezoneOffset('11:00:00+5.5')).toBe('+05:30');
155
+ });
156
+
157
+ it('parses negative decimal offset format', () => {
158
+ expect(parseTimezoneOffset('11:00:00-5.5')).toBe('-05:30');
159
+ });
160
+
161
+ it('parses simple integer offset', () => {
162
+ expect(parseTimezoneOffset('11:00:00+7')).toBe('+07:00');
163
+ });
164
+
165
+ it('parses zero offset', () => {
166
+ expect(parseTimezoneOffset('11:00:00+0')).toBe('+00:00');
167
+ });
168
+
169
+ it('returns empty for time without offset', () => {
170
+ expect(parseTimezoneOffset('11:00:00')).toBe('');
171
+ });
172
+
173
+ it('handles NaN offset values', () => {
174
+ expect(parseTimezoneOffset('11:00:00+abc')).toBe('');
175
+ });
176
+ });
177
+
178
+ describe('formatTimezoneOffset', () => {
179
+ it('returns empty string for empty input', () => {
180
+ expect(formatTimezoneOffset('')).toBe('');
181
+ });
182
+
183
+ it('formats positive offset with UTC prefix', () => {
184
+ expect(formatTimezoneOffset('11:00:00+07:00')).toBe('UTC+07:00');
185
+ });
186
+
187
+ it('formats negative offset with UTC prefix', () => {
188
+ expect(formatTimezoneOffset('14:30:00-05:30')).toBe('UTC-05:30');
189
+ });
190
+
191
+ it('formats decimal offset', () => {
192
+ expect(formatTimezoneOffset('11:00:00+5.5')).toBe('UTC+05:30');
193
+ });
194
+
195
+ it('returns empty for time without offset', () => {
196
+ expect(formatTimezoneOffset('11:00:00')).toBe('');
197
+ });
198
+ });
199
+
200
+ describe('getDateRange', () => {
201
+ beforeEach(() => {
202
+ vi.useFakeTimers();
203
+ vi.setSystemTime(new Date('2024-06-15T12:00:00Z'));
204
+ });
205
+
206
+ afterEach(() => {
207
+ vi.useRealTimers();
208
+ });
209
+
210
+ describe('day unit', () => {
211
+ it('returns today range for present option', () => {
212
+ const [start, end] = getDateRange('day', 'present');
213
+ expect(start?.getDate()).toBe(15);
214
+ expect(end?.getDate()).toBe(15);
215
+ });
216
+
217
+ it('returns yesterday range for past option', () => {
218
+ const [start, end] = getDateRange('day', 'past');
219
+ expect(start?.getDate()).toBe(14);
220
+ expect(end?.getDate()).toBe(14);
221
+ });
222
+
223
+ it('returns tomorrow range for future option', () => {
224
+ const [start, end] = getDateRange('day', 'future');
225
+ expect(start?.getDate()).toBe(16);
226
+ expect(end?.getDate()).toBe(16);
227
+ });
228
+ });
229
+
230
+ describe('week unit', () => {
231
+ it('returns this week range for present option', () => {
232
+ const [start, end] = getDateRange('week', 'present');
233
+ expect(start).toBeInstanceOf(Date);
234
+ expect(end).toBeInstanceOf(Date);
235
+ expect(end!.getTime()).toBeGreaterThan(start!.getTime());
236
+ });
237
+
238
+ it('returns last week range for past option', () => {
239
+ const [start, end] = getDateRange('week', 'past');
240
+ expect(start).toBeInstanceOf(Date);
241
+ expect(end).toBeInstanceOf(Date);
242
+ });
243
+
244
+ it('returns next week range for future option', () => {
245
+ const [start, end] = getDateRange('week', 'future');
246
+ expect(start).toBeInstanceOf(Date);
247
+ expect(end).toBeInstanceOf(Date);
248
+ });
249
+ });
250
+
251
+ describe('month unit', () => {
252
+ it('returns this month range for present option', () => {
253
+ const [start, end] = getDateRange('month', 'present');
254
+ expect(start?.getMonth()).toBe(5); // June (0-indexed)
255
+ expect(end?.getMonth()).toBe(5);
256
+ });
257
+
258
+ it('returns last month range for past option', () => {
259
+ const [start, end] = getDateRange('month', 'past');
260
+ expect(start?.getMonth()).toBe(4); // May
261
+ expect(end?.getMonth()).toBe(4);
262
+ });
263
+
264
+ it('returns next month range for future option', () => {
265
+ const [start, end] = getDateRange('month', 'future');
266
+ expect(start?.getMonth()).toBe(6); // July
267
+ expect(end?.getMonth()).toBe(6);
268
+ });
269
+ });
270
+
271
+ describe('year unit', () => {
272
+ it('returns this year range for present option', () => {
273
+ const [start, end] = getDateRange('year', 'present');
274
+ expect(start?.getFullYear()).toBe(2024);
275
+ expect(end?.getFullYear()).toBe(2024);
276
+ });
277
+
278
+ it('returns last year range for past option', () => {
279
+ const [start, end] = getDateRange('year', 'past');
280
+ expect(start?.getFullYear()).toBe(2023);
281
+ expect(end?.getFullYear()).toBe(2023);
282
+ });
283
+
284
+ it('returns next year range for future option', () => {
285
+ const [start, end] = getDateRange('year', 'future');
286
+ expect(start?.getFullYear()).toBe(2025);
287
+ expect(end?.getFullYear()).toBe(2025);
288
+ });
289
+ });
290
+
291
+ describe('all unit', () => {
292
+ it('returns null dates for all option', () => {
293
+ const [start, end] = getDateRange('all', 'present');
294
+ expect(start).toBeNull();
295
+ expect(end).toBeNull();
296
+ });
297
+ });
298
+
299
+ describe('custom unit', () => {
300
+ it('throws error for custom option', () => {
301
+ expect(() => getDateRange('custom', 'present')).toThrow(
302
+ 'Not implemented yet'
303
+ );
304
+ });
305
+ });
306
+ });
307
+
308
+ describe('getDateRangeUnits', () => {
309
+ it('returns all date range units', () => {
310
+ const mockT = (key: string) => key;
311
+ const units = getDateRangeUnits(mockT);
312
+
313
+ expect(units).toHaveLength(6);
314
+ expect(units.map((u) => u.value)).toEqual([
315
+ 'day',
316
+ 'week',
317
+ 'month',
318
+ 'year',
319
+ 'all',
320
+ 'custom',
321
+ ]);
322
+ });
323
+
324
+ it('uses translation function for labels', () => {
325
+ const mockT = (key: string) => `translated_${key}`;
326
+ const units = getDateRangeUnits(mockT);
327
+
328
+ expect(units[0]?.label).toBe('translated_date_helper.day');
329
+ });
330
+ });
331
+
332
+ describe('getDateRangeOptions', () => {
333
+ const mockT = (key: string) => key;
334
+
335
+ it('returns day options', () => {
336
+ const options = getDateRangeOptions('day', mockT);
337
+ expect(options).toHaveLength(3);
338
+ expect(options.map((o) => o.value)).toEqual([
339
+ 'present',
340
+ 'past',
341
+ 'future',
342
+ ]);
343
+ });
344
+
345
+ it('returns week options', () => {
346
+ const options = getDateRangeOptions('week', mockT);
347
+ expect(options).toHaveLength(3);
348
+ });
349
+
350
+ it('returns month options', () => {
351
+ const options = getDateRangeOptions('month', mockT);
352
+ expect(options).toHaveLength(3);
353
+ });
354
+
355
+ it('returns year options', () => {
356
+ const options = getDateRangeOptions('year', mockT);
357
+ expect(options).toHaveLength(3);
358
+ });
359
+
360
+ it('returns single option for all unit', () => {
361
+ const options = getDateRangeOptions('all', mockT);
362
+ expect(options).toHaveLength(1);
363
+ expect(options[0]?.value).toBe('present');
364
+ });
365
+
366
+ it('returns empty array for custom unit', () => {
367
+ const options = getDateRangeOptions('custom', mockT);
368
+ expect(options).toHaveLength(0);
369
+ });
370
+ });
371
+
372
+ describe('calculateDuration', () => {
373
+ it('formats seconds for short durations', () => {
374
+ const start = new Date('2024-01-15T10:00:00');
375
+ const end = new Date('2024-01-15T10:00:30');
376
+ expect(calculateDuration(start, end)).toBe('30 seconds');
377
+ });
378
+
379
+ it('formats minutes and seconds', () => {
380
+ const start = new Date('2024-01-15T10:00:00');
381
+ const end = new Date('2024-01-15T10:05:30');
382
+ expect(calculateDuration(start, end)).toBe('5m 30s');
383
+ });
384
+
385
+ it('formats hours, minutes, and seconds', () => {
386
+ const start = new Date('2024-01-15T10:00:00');
387
+ const end = new Date('2024-01-15T12:30:45');
388
+ expect(calculateDuration(start, end)).toBe('2h 30m 45s');
389
+ });
390
+
391
+ it('handles zero duration', () => {
392
+ const date = new Date('2024-01-15T10:00:00');
393
+ expect(calculateDuration(date, date)).toBe('0 seconds');
394
+ });
395
+
396
+ it('handles exactly one minute', () => {
397
+ const start = new Date('2024-01-15T10:00:00');
398
+ const end = new Date('2024-01-15T10:01:00');
399
+ expect(calculateDuration(start, end)).toBe('1m 0s');
400
+ });
401
+
402
+ it('handles exactly one hour', () => {
403
+ const start = new Date('2024-01-15T10:00:00');
404
+ const end = new Date('2024-01-15T11:00:00');
405
+ expect(calculateDuration(start, end)).toBe('1h 0m 0s');
406
+ });
407
+ });
408
+ });