hevy-shared 1.0.962 → 1.0.964

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 (86) hide show
  1. package/README.md +17 -2
  2. package/built/API/APIClient.d.ts +157 -0
  3. package/built/API/APIClient.js +381 -0
  4. package/built/API/index.d.ts +2 -0
  5. package/built/API/index.js +18 -0
  6. package/built/API/types.d.ts +38 -0
  7. package/built/API/types.js +18 -0
  8. package/built/adjustEventTokens.d.ts +16 -0
  9. package/built/adjustEventTokens.js +18 -0
  10. package/built/adminPermissions.d.ts +4 -0
  11. package/built/adminPermissions.js +22 -0
  12. package/built/async.d.ts +50 -0
  13. package/built/async.js +170 -0
  14. package/built/chat.d.ts +25 -23
  15. package/built/coachPlans.d.ts +2 -1
  16. package/built/coachPlans.js +2 -2
  17. package/built/cue.d.ts +12 -0
  18. package/built/cue.js +22 -0
  19. package/built/exerciseLocaleUtils.d.ts +17 -0
  20. package/built/exerciseLocaleUtils.js +62 -0
  21. package/built/filterExercises.d.ts +19 -3
  22. package/built/filterExercises.js +72 -60
  23. package/built/hevyTrainer.d.ts +250 -0
  24. package/built/hevyTrainer.js +676 -0
  25. package/built/index.d.ts +1217 -304
  26. package/built/index.js +268 -75
  27. package/built/muscleHeatmaps.d.ts +31 -0
  28. package/built/muscleHeatmaps.js +68 -0
  29. package/built/muscleSplits.d.ts +36 -0
  30. package/built/muscleSplits.js +100 -0
  31. package/built/normalizedWorkoutUtils.d.ts +88 -0
  32. package/built/normalizedWorkoutUtils.js +112 -0
  33. package/built/notifications.d.ts +215 -0
  34. package/built/notifications.js +9 -0
  35. package/built/routineUtils.d.ts +14 -0
  36. package/built/routineUtils.js +186 -0
  37. package/built/setIndicatorUtils.d.ts +4 -3
  38. package/built/setIndicatorUtils.js +15 -1
  39. package/built/tests/async.test.d.ts +1 -0
  40. package/built/tests/async.test.js +49 -0
  41. package/built/tests/hevyTrainer.test.d.ts +1 -0
  42. package/built/tests/hevyTrainer.test.js +1199 -0
  43. package/built/tests/muscleSplit.test.d.ts +1 -0
  44. package/built/tests/muscleSplit.test.js +153 -0
  45. package/built/tests/routineUtils.test.d.ts +1 -0
  46. package/built/tests/routineUtils.test.js +745 -0
  47. package/built/tests/testUtils.d.ts +85 -0
  48. package/built/tests/testUtils.js +319 -0
  49. package/built/tests/utils.test.js +748 -0
  50. package/built/tests/workoutVolume.test.js +165 -49
  51. package/built/translations/index.d.ts +2 -0
  52. package/built/translations/index.js +18 -0
  53. package/built/translations/translationUtils.d.ts +2 -0
  54. package/built/translations/translationUtils.js +61 -0
  55. package/built/translations/types.d.ts +8 -0
  56. package/built/translations/types.js +20 -0
  57. package/built/typeUtils.d.ts +70 -0
  58. package/built/typeUtils.js +55 -0
  59. package/built/units.d.ts +14 -7
  60. package/built/units.js +24 -14
  61. package/built/utils.d.ts +192 -5
  62. package/built/utils.js +598 -85
  63. package/built/websocket.d.ts +14 -2
  64. package/built/workoutVolume.d.ts +24 -5
  65. package/built/workoutVolume.js +25 -34
  66. package/package.json +30 -9
  67. package/.eslintignore +0 -2
  68. package/.eslintrc +0 -21
  69. package/.github/workflows/ci.yml +0 -15
  70. package/.github/workflows/npm-publish.yml +0 -59
  71. package/.github/workflows/pr-auto-assign.yml +0 -15
  72. package/.prettierrc.js +0 -5
  73. package/jest.config.js +0 -4
  74. package/src/chat.ts +0 -130
  75. package/src/coachPlans.ts +0 -57
  76. package/src/constants.ts +0 -14
  77. package/src/filterExercises.ts +0 -222
  78. package/src/index.ts +0 -1576
  79. package/src/setIndicatorUtils.ts +0 -137
  80. package/src/tests/utils.test.ts +0 -156
  81. package/src/tests/workoutVolume.test.ts +0 -93
  82. package/src/units.ts +0 -41
  83. package/src/utils.ts +0 -516
  84. package/src/websocket.ts +0 -36
  85. package/src/workoutVolume.ts +0 -175
  86. package/tsconfig.json +0 -70
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const dayjs_1 = __importDefault(require("dayjs"));
3
7
  const utils_1 = require("../utils");
4
8
  describe('utils', () => {
5
9
  describe('isValidUsername', () => {
@@ -15,6 +19,7 @@ describe('utils', () => {
15
19
  expect((0, utils_1.isValidUsername)('desmond-mc')).toBe(false);
16
20
  expect((0, utils_1.isValidUsername)('.desmondmc')).toBe(false);
17
21
  expect((0, utils_1.isValidUsername)('thisusernameistolongg')).toBe(false);
22
+ expect((0, utils_1.isValidUsername)('Desmond')).toBe(false);
18
23
  });
19
24
  });
20
25
  describe('isValidEmail', () => {
@@ -30,16 +35,37 @@ describe('utils', () => {
30
35
  });
31
36
  describe('isValidUrl', () => {
32
37
  it('returns true for valid url', () => {
38
+ expect((0, utils_1.isValidWebUrl)('0-0.com')).toBe(true);
39
+ expect((0, utils_1.isValidWebUrl)('0--0.com')).toBe(true);
40
+ expect((0, utils_1.isValidWebUrl)('a.aa')).toBe(true);
41
+ expect((0, utils_1.isValidWebUrl)('https://a.aa')).toBe(true);
42
+ expect((0, utils_1.isValidWebUrl)('https://a.aa/')).toBe(true);
43
+ expect((0, utils_1.isValidWebUrl)('w.a.aa')).toBe(true);
44
+ expect((0, utils_1.isValidWebUrl)('https://w.a.aa')).toBe(true);
45
+ expect((0, utils_1.isValidWebUrl)('https://w.a.aa/')).toBe(true);
33
46
  expect((0, utils_1.isValidWebUrl)('http://hevyapp.com')).toBe(true);
34
47
  expect((0, utils_1.isValidWebUrl)('hevyapp.com')).toBe(true);
35
48
  expect((0, utils_1.isValidWebUrl)('https://hevyapp.com')).toBe(true);
36
49
  expect((0, utils_1.isValidWebUrl)('hevyapp.com/')).toBe(true);
50
+ expect((0, utils_1.isValidWebUrl)('https://hevyapp.com/')).toBe(true);
37
51
  expect((0, utils_1.isValidWebUrl)('hevyapp.com/about-us')).toBe(true);
38
52
  expect((0, utils_1.isValidWebUrl)('www.hevyapp.com')).toBe(true);
39
53
  expect((0, utils_1.isValidWebUrl)('something.xd')).toBe(true);
40
54
  expect((0, utils_1.isValidWebUrl)('my-cool.website')).toBe(true);
41
55
  });
42
56
  it('returns false for invalid url', () => {
57
+ expect((0, utils_1.isValidWebUrl)('-.com')).toBe(false);
58
+ expect((0, utils_1.isValidWebUrl)('0-.com')).toBe(false);
59
+ expect((0, utils_1.isValidWebUrl)('-0.com')).toBe(false);
60
+ expect((0, utils_1.isValidWebUrl)('a.a')).toBe(false);
61
+ expect((0, utils_1.isValidWebUrl)('a..aa')).toBe(false);
62
+ expect((0, utils_1.isValidWebUrl)('.a.aa')).toBe(false);
63
+ expect((0, utils_1.isValidWebUrl)('..a.aa')).toBe(false);
64
+ expect((0, utils_1.isValidWebUrl)('a.aa.')).toBe(false);
65
+ expect((0, utils_1.isValidWebUrl)('hevyapp.com:1234')).toBe(false);
66
+ expect((0, utils_1.isValidWebUrl)('hevyapp.com:1234/')).toBe(false);
67
+ expect((0, utils_1.isValidWebUrl)('https://hevyapp.com:1234')).toBe(false);
68
+ expect((0, utils_1.isValidWebUrl)('https://hevyapp.com:1234/')).toBe(false);
43
69
  expect((0, utils_1.isValidWebUrl)('asdasd')).toBe(false);
44
70
  expect((0, utils_1.isValidWebUrl)('ftp://hevyapp.com')).toBe(false);
45
71
  expect((0, utils_1.isValidWebUrl)('mailto://me@me.com')).toBe(false);
@@ -51,6 +77,10 @@ describe('utils', () => {
51
77
  expect((0, utils_1.isValidWebUrl)('This is my link')).toBe(false);
52
78
  expect((0, utils_1.isValidWebUrl)('my_impossible.website')).toBe(false);
53
79
  });
80
+ it('returns false when the entire string is not a valid url', () => {
81
+ expect((0, utils_1.isValidWebUrl)(' https://a.aa')).toBe(false);
82
+ expect((0, utils_1.isValidWebUrl)('https://a.aa ')).toBe(false);
83
+ });
54
84
  });
55
85
  describe('isValidPhoneNumber', () => {
56
86
  it('returns true for a valid phone number', () => {
@@ -68,6 +98,76 @@ describe('utils', () => {
68
98
  expect((0, utils_1.isValidPhoneNumber)('')).toBe(false);
69
99
  });
70
100
  });
101
+ describe('isNumber', () => {
102
+ it('returns true for a number', () => {
103
+ expect((0, utils_1.isNumber)(1)).toBe(true);
104
+ expect((0, utils_1.isNumber)(0)).toBe(true);
105
+ expect((0, utils_1.isNumber)(100)).toBe(true);
106
+ expect((0, utils_1.isNumber)(1000000000)).toBe(true);
107
+ expect((0, utils_1.isNumber)(-1)).toBe(true);
108
+ expect((0, utils_1.isNumber)(-100)).toBe(true);
109
+ expect((0, utils_1.isNumber)(-1000000000.0)).toBe(true);
110
+ expect((0, utils_1.isNumber)(9.0)).toBe(true);
111
+ expect((0, utils_1.isNumber)(1.1)).toBe(true);
112
+ expect((0, utils_1.isNumber)(0.1)).toBe(true);
113
+ expect((0, utils_1.isNumber)(0.0000001)).toBe(true);
114
+ expect((0, utils_1.isNumber)(0.111111)).toBe(true);
115
+ expect((0, utils_1.isNumber)(100.1)).toBe(true);
116
+ expect((0, utils_1.isNumber)(1000000000.1)).toBe(true);
117
+ expect((0, utils_1.isNumber)(-1.1)).toBe(true);
118
+ expect((0, utils_1.isNumber)((5 * 8) / 689)).toBe(true);
119
+ expect((0, utils_1.isNumber)(Infinity)).toBe(true);
120
+ expect((0, utils_1.isNumber)(-Infinity)).toBe(true);
121
+ expect((0, utils_1.isNumber)(NaN)).toBe(true);
122
+ });
123
+ it('returns false for a non-number', () => {
124
+ expect((0, utils_1.isNumber)('')).toBe(false);
125
+ expect((0, utils_1.isNumber)('1')).toBe(false);
126
+ expect((0, utils_1.isNumber)(null)).toBe(false);
127
+ expect((0, utils_1.isNumber)(undefined)).toBe(false);
128
+ expect((0, utils_1.isNumber)(false)).toBe(false);
129
+ expect((0, utils_1.isNumber)(true)).toBe(false);
130
+ expect((0, utils_1.isNumber)({})).toBe(false);
131
+ expect((0, utils_1.isNumber)([])).toBe(false);
132
+ expect((0, utils_1.isNumber)({ number: 8 })).toBe(false);
133
+ expect((0, utils_1.isNumber)('1')).toBe(false);
134
+ expect((0, utils_1.isNumber)('0')).toBe(false);
135
+ expect((0, utils_1.isNumber)('100')).toBe(false);
136
+ expect((0, utils_1.isNumber)('1000000000')).toBe(false);
137
+ expect((0, utils_1.isNumber)('-1')).toBe(false);
138
+ expect((0, utils_1.isNumber)('-100')).toBe(false);
139
+ expect((0, utils_1.isNumber)('-1000000000.0')).toBe(false);
140
+ expect((0, utils_1.isNumber)('9.0')).toBe(false);
141
+ expect((0, utils_1.isNumber)('1.1')).toBe(false);
142
+ expect((0, utils_1.isNumber)('0.1')).toBe(false);
143
+ expect((0, utils_1.isNumber)('0.0000001')).toBe(false);
144
+ expect((0, utils_1.isNumber)('0.111111')).toBe(false);
145
+ expect((0, utils_1.isNumber)('100.1')).toBe(false);
146
+ expect((0, utils_1.isNumber)('1000000000.1')).toBe(false);
147
+ expect((0, utils_1.isNumber)('-1.1')).toBe(false);
148
+ });
149
+ });
150
+ describe('isWholeNumber', () => {
151
+ it('returns true for a whole number', () => {
152
+ expect((0, utils_1.isWholeNumber)(1)).toBe(true);
153
+ expect((0, utils_1.isWholeNumber)(0)).toBe(true);
154
+ expect((0, utils_1.isWholeNumber)(100)).toBe(true);
155
+ expect((0, utils_1.isWholeNumber)(1000000000)).toBe(true);
156
+ expect((0, utils_1.isWholeNumber)(-1)).toBe(true);
157
+ expect((0, utils_1.isWholeNumber)(-100)).toBe(true);
158
+ expect((0, utils_1.isWholeNumber)(-1000000000.0)).toBe(true);
159
+ expect((0, utils_1.isWholeNumber)(9.0)).toBe(true);
160
+ });
161
+ it('returns false for a non-whole number', () => {
162
+ expect((0, utils_1.isWholeNumber)(1.1)).toBe(false);
163
+ expect((0, utils_1.isWholeNumber)(0.1)).toBe(false);
164
+ expect((0, utils_1.isWholeNumber)(0.0000001)).toBe(false);
165
+ expect((0, utils_1.isWholeNumber)(0.111111)).toBe(false);
166
+ expect((0, utils_1.isWholeNumber)(100.1)).toBe(false);
167
+ expect((0, utils_1.isWholeNumber)(1000000000.1)).toBe(false);
168
+ expect((0, utils_1.isWholeNumber)(-1.1)).toBe(false);
169
+ });
170
+ });
71
171
  describe('forceStringToNumber', () => {
72
172
  it('Takes any string and outputs a number. No matter what.', () => {
73
173
  expect((0, utils_1.forceStringToNumber)('10')).toBe(10);
@@ -98,6 +198,143 @@ describe('utils', () => {
98
198
  expect((0, utils_1.stringToNumber)('')).toBe(undefined);
99
199
  });
100
200
  });
201
+ describe('findMapped', () => {
202
+ it('Returns the first non-undefined value produced by transform function being applied to array elements', () => {
203
+ expect((0, utils_1.findMapped)([1, 2, 3, 4, 5], (e) => (e >= 3 ? e * 10 : undefined))).toBe(30);
204
+ expect((0, utils_1.findMapped)([1, 2, 3, 4, 5], (e) => (e >= 3 ? String(e) : undefined))).toBe('3');
205
+ expect((0, utils_1.findMapped)(['a: element 001.', 'b: element 002.', 'c: element 003.'], (e) => { var _a; return (_a = e.match(/^c: [^0-9]*([0-9]+).*$/)) === null || _a === void 0 ? void 0 : _a[1]; })).toBe('003');
206
+ });
207
+ it('Returns `undefined` if no non-undefined value was produced by transform function being applied to array elements', () => {
208
+ expect((0, utils_1.findMapped)([1, 2, 3, 4, 5], () => undefined)).toBeUndefined();
209
+ expect((0, utils_1.findMapped)([], () => true)).toBeUndefined();
210
+ expect((0, utils_1.findMapped)([], () => undefined)).toBeUndefined();
211
+ });
212
+ });
213
+ describe('toFragmentedJSON', () => {
214
+ it('converts any array into an array of JSON chunks with a maximum given length', () => {
215
+ expect((0, utils_1.toFragmentedJSON)([
216
+ { value: 'AAAAAAAAAAAAAAAA' },
217
+ { value: 'BBBBBBBBBBBBBBBBBBBBB' },
218
+ { value: 'CCCCCCCCCCCCCCCC' },
219
+ ], 64)).toEqual([
220
+ JSON.stringify([
221
+ { value: 'AAAAAAAAAAAAAAAA' },
222
+ { value: 'BBBBBBBBBBBBBBBBBBBBB' },
223
+ ]),
224
+ JSON.stringify([{ value: 'CCCCCCCCCCCCCCCC' }]),
225
+ ]);
226
+ expect((0, utils_1.toFragmentedJSON)([
227
+ { value: 'AAAAAAAAAAAAAAAA' },
228
+ { value: 'BBBBBBBBBBBBBBBBBBBBB' },
229
+ { value: 'CCCCCCCCCCCCCCCC' },
230
+ ], 35)).toEqual([
231
+ JSON.stringify([{ value: 'AAAAAAAAAAAAAAAA' }]),
232
+ JSON.stringify([{ value: 'BBBBBBBBBBBBBBBBBBBBB' }]),
233
+ JSON.stringify([{ value: 'CCCCCCCCCCCCCCCC' }]),
234
+ ]);
235
+ expect(() => (0, utils_1.toFragmentedJSON)([
236
+ { value: 'AAAAAAAAAAAAAAAA' },
237
+ { value: 'BBBBBBBBBBBBBBBBBBBBB' },
238
+ { value: 'CCCCCCCCCCCCCCCC' },
239
+ ], 34)).toThrow();
240
+ expect((0, utils_1.toFragmentedJSON)(['A', 'B', 'C'], 15)).toEqual([
241
+ JSON.stringify(['A', 'B', 'C']),
242
+ ]);
243
+ expect((0, utils_1.toFragmentedJSON)(['A', 'B', 'C'], 10)).toEqual([
244
+ JSON.stringify(['A', 'B']),
245
+ JSON.stringify(['C']),
246
+ ]);
247
+ expect((0, utils_1.toFragmentedJSON)(['A', 'B', 'C'], 5)).toEqual([
248
+ JSON.stringify(['A']),
249
+ JSON.stringify(['B']),
250
+ JSON.stringify(['C']),
251
+ ]);
252
+ expect(() => (0, utils_1.toFragmentedJSON)(['A', 'B', 'C'], 1)).toThrow();
253
+ expect(() => (0, utils_1.toFragmentedJSON)(['A', 'B', 'C'], 0)).toThrow();
254
+ expect((0, utils_1.toFragmentedJSON)([], 1)).toEqual([]);
255
+ expect((0, utils_1.toFragmentedJSON)([], 0)).toEqual([]);
256
+ });
257
+ it('returns an empty array for an empty input array', () => {
258
+ expect((0, utils_1.toFragmentedJSON)(['blah'], 100)).toEqual([
259
+ JSON.stringify(['blah']),
260
+ ]);
261
+ expect((0, utils_1.toFragmentedJSON)([], 100)).toEqual([]);
262
+ expect((0, utils_1.toFragmentedJSON)([], 100)).not.toEqual([JSON.stringify([])]);
263
+ });
264
+ });
265
+ describe('getClosestDataPointBeforeTargetDate', () => {
266
+ it('gets the closest data point before a target date, if such a data point exists, using a function to extract the date from the data points', () => {
267
+ var _a, _b, _c, _d;
268
+ expect((0, utils_1.getClosestDataPointBeforeTargetDate)([
269
+ { id: 1, date: new Date('2023-01-01') },
270
+ { id: 2, date: new Date('2023-04-01') },
271
+ ], (item) => new Date(item.date), new Date('2023-03-15'))).toEqual({ id: 1, date: new Date('2023-01-01') });
272
+ expect((_a = (0, utils_1.getClosestDataPointBeforeTargetDate)([{ key: 'abc', date: '2022-10-11' }], (item) => new Date(item.date), new Date('2022-12-21'))) === null || _a === void 0 ? void 0 : _a.key).toEqual('abc');
273
+ expect((_b = (0, utils_1.getClosestDataPointBeforeTargetDate)([
274
+ { key: 'wrong1', date: '2022-09-05' },
275
+ { key: 'wrong2', date: '2022-09-06' },
276
+ { key: 'wrong3', date: '2022-09-07' },
277
+ { key: 'wrong4', date: '2022-09-08' },
278
+ { key: 'closest', date: '2022-09-09' },
279
+ { key: 'wrong5', date: '2022-09-11' },
280
+ { key: 'wrong6', date: '2022-09-12' },
281
+ { key: 'wrong7', date: '2022-09-13' },
282
+ ], (item) => new Date(item.date), new Date('2022-09-10'))) === null || _b === void 0 ? void 0 : _b.key).toEqual('closest');
283
+ expect((_c = (0, utils_1.getClosestDataPointBeforeTargetDate)([
284
+ { key: 'wrong1', date: '2022-09-05' },
285
+ { key: 'wrong2', date: '2022-09-06' },
286
+ { key: 'wrong3', date: '2022-09-07' },
287
+ { key: 'wrong4', date: '2022-09-08' },
288
+ { key: 'wrong5', date: '2022-09-09' },
289
+ { key: 'exact', date: '2022-09-10' },
290
+ { key: 'wrong6', date: '2022-09-11' },
291
+ { key: 'wrong7', date: '2022-09-12' },
292
+ { key: 'wrong8', date: '2022-09-13' },
293
+ ], (item) => new Date(item.date), new Date('2022-09-10'))) === null || _c === void 0 ? void 0 : _c.key).toEqual('exact');
294
+ expect((_d = (0, utils_1.getClosestDataPointBeforeTargetDate)([{ key: 'abc', date: '2023-01-15' }], (item) => new Date(item.date), new Date('2022-12-21'))) === null || _d === void 0 ? void 0 : _d.key).toBeUndefined();
295
+ });
296
+ });
297
+ describe('getClosestDataPointAroundTargetDate', () => {
298
+ it('gets the closest data point to a target date, if such a data point exists, using a function to extract the date from the data points', () => {
299
+ var _a, _b, _c, _d;
300
+ expect((0, utils_1.getClosestDataPointAroundTargetDate)([
301
+ { id: 1, date: new Date('2023-01-01') },
302
+ { id: 2, date: new Date('2023-04-01') },
303
+ ], (item) => new Date(item.date), new Date('2023-03-15'))).toEqual({ id: 2, date: new Date('2023-04-01') });
304
+ // Test single point - should return it as closest
305
+ expect((_a = (0, utils_1.getClosestDataPointAroundTargetDate)([{ key: 'abc', date: '2022-10-11' }], (item) => new Date(item.date), new Date('2022-12-21'))) === null || _a === void 0 ? void 0 : _a.key).toEqual('abc');
306
+ // Test multiple points - should return closest by absolute difference
307
+ expect((_b = (0, utils_1.getClosestDataPointAroundTargetDate)([
308
+ { key: 'wrong1', date: '2022-09-05' },
309
+ { key: 'wrong2', date: '2022-09-06' },
310
+ { key: 'wrong3', date: '2022-09-07' },
311
+ { key: 'wrong4', date: '2022-09-08' },
312
+ { key: 'closest', date: '2022-09-09' }, // 1 day before
313
+ { key: 'alsoClose', date: '2022-09-11' }, // 1 day after
314
+ { key: 'wrong6', date: '2022-09-12' },
315
+ { key: 'wrong7', date: '2022-09-13' },
316
+ ], (item) => new Date(item.date), new Date('2022-09-10'))) === null || _b === void 0 ? void 0 : _b.key).toEqual('closest'); // Should pick the earlier date when equidistant
317
+ // Test exact match
318
+ expect((_c = (0, utils_1.getClosestDataPointAroundTargetDate)([
319
+ { key: 'wrong1', date: '2022-09-05' },
320
+ { key: 'wrong2', date: '2022-09-06' },
321
+ { key: 'wrong3', date: '2022-09-07' },
322
+ { key: 'wrong4', date: '2022-09-08' },
323
+ { key: 'wrong5', date: '2022-09-09' },
324
+ { key: 'exact', date: '2022-09-10' },
325
+ { key: 'wrong6', date: '2022-09-11' },
326
+ { key: 'wrong7', date: '2022-09-12' },
327
+ ], (item) => new Date(item.date), new Date('2022-09-10'))) === null || _c === void 0 ? void 0 : _c.key).toEqual('exact');
328
+ // Test empty array
329
+ expect((0, utils_1.getClosestDataPointAroundTargetDate)([], (item) => new Date(item.date), new Date('2022-09-10'))).toBeUndefined();
330
+ // Test closest when target is between two dates
331
+ expect((_d = (0, utils_1.getClosestDataPointAroundTargetDate)([
332
+ { key: 'further', date: '2022-09-08' },
333
+ { key: 'closest', date: '2022-09-09' },
334
+ { key: 'secondClosest', date: '2022-09-12' },
335
+ ], (item) => new Date(item.date), new Date('2022-09-10'))) === null || _d === void 0 ? void 0 : _d.key).toEqual('closest');
336
+ });
337
+ });
101
338
  describe('removeAccents', () => {
102
339
  it('Takes an input string and returns a new string with accents removed', () => {
103
340
  expect((0, utils_1.removeAccents)('dog')).toBe('dog');
@@ -125,9 +362,520 @@ describe('utils', () => {
125
362
  expect((0, utils_1.isValidFormattedTime)('asdfg')).toBe(false);
126
363
  expect((0, utils_1.isValidFormattedTime)('0:00')).toBe(false);
127
364
  expect((0, utils_1.isValidFormattedTime)('0:00:01')).toBe(false);
365
+ expect((0, utils_1.isValidFormattedTime)('2:10')).toBe(false);
128
366
  expect((0, utils_1.isValidFormattedTime)('99:99')).toBe(true);
129
367
  expect((0, utils_1.isValidFormattedTime)('00:01')).toBe(true);
130
368
  expect((0, utils_1.isValidFormattedTime)('10:00:01')).toBe(true);
131
369
  });
132
370
  });
371
+ describe('formatDurationInput', () => {
372
+ it('Returns a string in the format of NN:NN or NN:NN:NN', () => {
373
+ expect((0, utils_1.formatDurationInput)('0')).toBe('');
374
+ expect((0, utils_1.formatDurationInput)('1')).toBe('00:01');
375
+ expect((0, utils_1.formatDurationInput)('60')).toBe(undefined);
376
+ expect((0, utils_1.formatDurationInput)('61')).toBe(undefined);
377
+ expect((0, utils_1.formatDurationInput)('99:99')).toBe('99:99');
378
+ });
379
+ });
380
+ describe('getEstimatedExercisesDurationSeconds', () => {
381
+ const testCases = [
382
+ {
383
+ exercises: [
384
+ {
385
+ exercise_type: 'duration',
386
+ rest_seconds: 10,
387
+ sets: [{ duration_seconds: 50, indicator: 'normal' }],
388
+ },
389
+ ],
390
+ expectedDuration: 50 + 10,
391
+ },
392
+ {
393
+ exercises: [
394
+ {
395
+ exercise_type: 'floors_duration',
396
+ rest_seconds: 10,
397
+ sets: [{ duration_seconds: 0, indicator: 'normal' }],
398
+ },
399
+ ],
400
+ expectedDuration: 10,
401
+ },
402
+ {
403
+ exercises: [
404
+ {
405
+ exercise_type: 'weight_duration',
406
+ rest_seconds: null,
407
+ sets: [{ duration_seconds: 30, indicator: 'normal' }],
408
+ },
409
+ ],
410
+ expectedDuration: 30 + utils_1.ESTIMATED_REST_TIMER_DURATION,
411
+ },
412
+ {
413
+ exercises: [
414
+ {
415
+ exercise_type: 'distance_duration',
416
+ rest_seconds: 120,
417
+ sets: [{ duration_seconds: 40, indicator: 'normal' }],
418
+ },
419
+ ],
420
+ expectedDuration: 40 + 120,
421
+ },
422
+ {
423
+ exercises: [
424
+ {
425
+ exercise_type: 'weight_duration',
426
+ rest_seconds: 120,
427
+ sets: [{ duration_seconds: null, indicator: 'normal' }],
428
+ },
429
+ ],
430
+ expectedDuration: 120 + utils_1.ESTIMATED_SET_DURATION,
431
+ },
432
+ {
433
+ exercises: [
434
+ {
435
+ exercise_type: 'duration',
436
+ rest_seconds: 0,
437
+ sets: [{ duration_seconds: null, indicator: 'normal' }],
438
+ },
439
+ ],
440
+ expectedDuration: utils_1.ESTIMATED_SET_DURATION,
441
+ },
442
+ {
443
+ exercises: [
444
+ {
445
+ exercise_type: 'reps_only',
446
+ rest_seconds: 180,
447
+ sets: [{ duration_seconds: null, indicator: 'normal' }],
448
+ },
449
+ {
450
+ exercise_type: 'reps_only',
451
+ rest_seconds: 180,
452
+ sets: [
453
+ {
454
+ duration_seconds: 0,
455
+ indicator: 'normal',
456
+ },
457
+ {
458
+ duration_seconds: 0,
459
+ indicator: 'normal',
460
+ },
461
+ {
462
+ duration_seconds: 0,
463
+ indicator: 'normal',
464
+ },
465
+ {
466
+ duration_seconds: 0,
467
+ indicator: 'normal',
468
+ },
469
+ ],
470
+ },
471
+ ],
472
+ expectedDuration: (180 + utils_1.ESTIMATED_SET_DURATION) * 5,
473
+ },
474
+ {
475
+ exercises: [
476
+ {
477
+ exercise_type: 'reps_only',
478
+ rest_seconds: null,
479
+ sets: [{ duration_seconds: null, indicator: 'normal' }],
480
+ },
481
+ {
482
+ exercise_type: 'reps_only',
483
+ rest_seconds: null,
484
+ sets: [
485
+ {
486
+ duration_seconds: 0,
487
+ indicator: 'normal',
488
+ },
489
+ {
490
+ duration_seconds: 0,
491
+ indicator: 'dropset',
492
+ },
493
+ {
494
+ duration_seconds: 0,
495
+ indicator: 'normal',
496
+ },
497
+ {
498
+ duration_seconds: 0,
499
+ indicator: 'dropset',
500
+ },
501
+ ],
502
+ },
503
+ ],
504
+ expectedDuration: utils_1.ESTIMATED_REST_TIMER_DURATION * 3 + 5 * utils_1.ESTIMATED_SET_DURATION,
505
+ },
506
+ ];
507
+ testCases.forEach((testCase) => {
508
+ it(`getEstimatedExercisesDurationSeconds`, () => {
509
+ expect((0, utils_1.getEstimatedExercisesDurationSeconds)(testCase)).toBe(testCase.expectedDuration);
510
+ });
511
+ });
512
+ });
513
+ describe('splitAtUsernamesAndLinks', () => {
514
+ it('Takes a string and returns an array of FormatAtText', () => {
515
+ const inputOutputs = [
516
+ [
517
+ 'No mentions in this',
518
+ [{ format: 'none', text: 'No mentions in this' }],
519
+ ],
520
+ [
521
+ 'Crazy workout with @guillemros',
522
+ [
523
+ { format: 'none', text: 'Crazy workout with ' },
524
+ { format: '@', text: '@guillemros' },
525
+ ],
526
+ ],
527
+ [
528
+ '@himynameisivo and @guillemros are crazy!',
529
+ [
530
+ { format: '@', text: '@himynameisivo' },
531
+ { format: 'none', text: ' and ' },
532
+ { format: '@', text: '@guillemros' },
533
+ { format: 'none', text: ' are crazy!' },
534
+ ],
535
+ ],
536
+ [
537
+ 'Worked out with @himynameisivo and @guillemros!',
538
+ [
539
+ { format: 'none', text: 'Worked out with ' },
540
+ { format: '@', text: '@himynameisivo' },
541
+ { format: 'none', text: ' and ' },
542
+ { format: '@', text: '@guillemros' },
543
+ { format: 'none', text: '!' },
544
+ ],
545
+ ],
546
+ [
547
+ '@himynameisivo @guillemros!',
548
+ [
549
+ { format: '@', text: '@himynameisivo' },
550
+ { format: 'none', text: ' ' },
551
+ { format: '@', text: '@guillemros' },
552
+ { format: 'none', text: '!' },
553
+ ],
554
+ ],
555
+ [
556
+ '@12345 @123_123!',
557
+ [
558
+ { format: '@', text: '@12345' },
559
+ { format: 'none', text: ' ' },
560
+ { format: '@', text: '@123_123' },
561
+ { format: 'none', text: '!' },
562
+ ],
563
+ ],
564
+ [
565
+ '@12345 www.google.com! hey',
566
+ [
567
+ { format: '@', text: '@12345' },
568
+ { format: 'none', text: ' ' },
569
+ { format: 'link', text: 'www.google.com' },
570
+ { format: 'none', text: '! hey' },
571
+ ],
572
+ ],
573
+ [
574
+ 'hey www.google.com youtube.com https://example.com',
575
+ [
576
+ { format: 'none', text: 'hey ' },
577
+ { format: 'link', text: 'www.google.com' },
578
+ { format: 'none', text: ' ' },
579
+ { format: 'link', text: 'youtube.com' },
580
+ { format: 'none', text: ' ' },
581
+ { format: 'link', text: 'https://example.com' },
582
+ ],
583
+ ],
584
+ [
585
+ 'hey Test - https://www.tiktok.com/@tylerpath/video/7471271437467405614 @test',
586
+ [
587
+ { format: 'none', text: 'hey Test - ' },
588
+ {
589
+ format: 'link',
590
+ text: 'https://www.tiktok.com/@tylerpath/video/7471271437467405614',
591
+ },
592
+ { format: 'none', text: ' ' },
593
+ { format: '@', text: '@test' },
594
+ ],
595
+ ],
596
+ ];
597
+ inputOutputs.forEach((i) => {
598
+ expect((0, utils_1.splitAtUsernamesAndLinks)(i[0])).toEqual(i[1]);
599
+ });
600
+ });
601
+ });
602
+ describe('generateUserGroup', () => {
603
+ it('generates a subsample or test group given a user id', () => {
604
+ expect((0, utils_1.generateUserGroup)('0a83932f-c3d4-4cbb-acfc-db48c09a362d', 2)).toEqual({
605
+ isSuccess: true,
606
+ value: Number(BigInt('0xdb48c09a362d') % BigInt(2)),
607
+ });
608
+ expect((0, utils_1.generateUserGroup)('2B022B1F-93C7-4C8F-9677-9F3000B29D2A', 3)).toEqual({
609
+ isSuccess: true,
610
+ value: Number(BigInt('0x9f3000b29d2a') % BigInt(3)),
611
+ });
612
+ expect((0, utils_1.generateUserGroup)('7693699a-afe7-4DEC-D07E-9694d2de3920', 1000)).toEqual({
613
+ isSuccess: true,
614
+ value: Number(BigInt('0x9694d2de3920') % BigInt(1000)),
615
+ });
616
+ });
617
+ it('fails when given an invalid input', () => {
618
+ expect((0, utils_1.generateUserGroup)('c1abee8a-461a-414c-9a2f-f1ba120cb8c7', 1)).toEqual({
619
+ isSuccess: false,
620
+ error: 'invalid-number-of-groups',
621
+ });
622
+ expect((0, utils_1.generateUserGroup)('absolutely-no-UUID-to-be-found-here', 2)).toEqual({
623
+ isSuccess: false,
624
+ error: 'invalid-uuid',
625
+ });
626
+ expect((0, utils_1.generateUserGroup)('3187b48c-d9bd-11ef-a2ec-325096b39f47', 2)).toEqual({
627
+ isSuccess: false,
628
+ error: 'uuid-not-v4',
629
+ });
630
+ expect((0, utils_1.generateUserGroup)('baa45206-475e-4bf5-7a9d-a21fd3d6966d', 2)).toEqual({
631
+ isSuccess: false,
632
+ error: 'invalid-variant',
633
+ });
634
+ });
635
+ });
636
+ describe('validateYoutubeUrl', () => {
637
+ it('returns true for valid YouTube URLs', () => {
638
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true);
639
+ expect((0, utils_1.validateYoutubeUrl)('https://youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true);
640
+ expect((0, utils_1.validateYoutubeUrl)('http://youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true);
641
+ expect((0, utils_1.validateYoutubeUrl)('youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true);
642
+ expect((0, utils_1.validateYoutubeUrl)('https://youtu.be/dQw4w9WgXcQ')).toBe(true);
643
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/shorts/dQw4w9WgXcQ')).toBe(true);
644
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/embed/dQw4w9WgXcQ')).toBe(true);
645
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/v/dQw4w9WgXcQ')).toBe(true);
646
+ expect((0, utils_1.validateYoutubeUrl)('https://youtube.com/shorts/dQw4w9WgXcQ')).toBe(true);
647
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s')).toBe(true);
648
+ });
649
+ it('returns false for invalid YouTube URLs', () => {
650
+ expect((0, utils_1.validateYoutubeUrl)('https://www.notbyoutube.com/watch?v=dQw4w9WgXcQ')).toBe(false);
651
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.org/watch?v=dQw4w9WgXcQ')).toBe(false);
652
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/notWatch?v=dQw4w9WgXcQ')).toBe(false);
653
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/watch?id=dQw4w9WgXcQ')).toBe(false);
654
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtube.com/watch?v=short')).toBe(false); // Too short ID
655
+ expect((0, utils_1.validateYoutubeUrl)('https://www.youtu.be/dQw4w9WgXcQ')).toBe(false); // Invalid domain
656
+ expect((0, utils_1.validateYoutubeUrl)('https://www.vimeo.com/123456789')).toBe(false);
657
+ expect((0, utils_1.validateYoutubeUrl)('')).toBe(false);
658
+ expect((0, utils_1.validateYoutubeUrl)('not a url')).toBe(false);
659
+ });
660
+ });
661
+ describe('getYoutubeVideoId', () => {
662
+ it('extracts video ID from standard YouTube URLs', () => {
663
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
664
+ expect((0, utils_1.getYoutubeVideoId)('https://youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
665
+ expect((0, utils_1.getYoutubeVideoId)('http://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
666
+ expect((0, utils_1.getYoutubeVideoId)('youtube.com/watch?v=dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
667
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s')).toBe('dQw4w9WgXcQ');
668
+ expect((0, utils_1.getYoutubeVideoId)('https://youtu.be/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
669
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/embed/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
670
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/v/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
671
+ });
672
+ it('extracts video ID from YouTube Shorts URLs', () => {
673
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/shorts/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
674
+ expect((0, utils_1.getYoutubeVideoId)('https://youtube.com/shorts/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
675
+ expect((0, utils_1.getYoutubeVideoId)('youtube.com/shorts/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
676
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/shorts/dQw4w9WgXcQ?feature=share')).toBe('dQw4w9WgXcQ');
677
+ });
678
+ it('returns undefined for invalid YouTube URLs', () => {
679
+ expect((0, utils_1.getYoutubeVideoId)('https://www.notYoutube.com/watch?v=dQw4w9WgXcQ')).toBeUndefined();
680
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.org/watch?v=dQw4w9WgXcQ')).toBeUndefined();
681
+ expect((0, utils_1.getYoutubeVideoId)('https://www.vimeo.com/123456789')).toBeUndefined();
682
+ expect((0, utils_1.getYoutubeVideoId)('')).toBeUndefined();
683
+ expect((0, utils_1.getYoutubeVideoId)('not a url')).toBeUndefined();
684
+ });
685
+ it('returns undefined for invalid video IDs', () => {
686
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=short')).toBeUndefined(); // ID too short
687
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/shorts/short')).toBeUndefined(); // ID too short
688
+ });
689
+ it('extracts video ID from videos in playlists', () => {
690
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoS')).toBe('dQw4w9WgXcQ');
691
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoS&index=2')).toBe('dQw4w9WgXcQ');
692
+ });
693
+ it('extracts video ID from videos from channels', () => {
694
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=channel')).toBe('dQw4w9WgXcQ');
695
+ expect((0, utils_1.getYoutubeVideoId)('https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstleyVEVO')).toBe('dQw4w9WgXcQ');
696
+ });
697
+ });
698
+ describe('calculateCurrentWeekStreak', () => {
699
+ it("Doesn't include gaps in the streak count", () => {
700
+ const workouts = [
701
+ { start_time: (0, dayjs_1.default)('2025-11-05').unix() },
702
+ { start_time: (0, dayjs_1.default)('2025-11-02').unix() },
703
+ { start_time: (0, dayjs_1.default)('2025-10-31').unix() },
704
+ { start_time: (0, dayjs_1.default)('2025-10-23').unix() },
705
+ { start_time: (0, dayjs_1.default)('2025-10-10').unix() },
706
+ ];
707
+ const firstWeekday = 'sunday';
708
+ const untilUnix = (0, dayjs_1.default)('2025-11-06').unix();
709
+ expect((0, utils_1.calculateCurrentWeekStreak)(workouts, firstWeekday, untilUnix)).toBe(3);
710
+ });
711
+ it("Calculates a potentially non-zero streak when the user hasn't worked out this week yet", () => {
712
+ const workouts = [
713
+ { start_time: (0, dayjs_1.default)('2025-10-31').unix() },
714
+ { start_time: (0, dayjs_1.default)('2025-10-23').unix() },
715
+ { start_time: (0, dayjs_1.default)('2025-10-20').unix() },
716
+ { start_time: (0, dayjs_1.default)('2025-10-14').unix() + 1 },
717
+ { start_time: (0, dayjs_1.default)('2025-10-12').unix() },
718
+ ];
719
+ const firstWeekday = 'monday';
720
+ const untilUnix = (0, dayjs_1.default)('2025-11-06').unix();
721
+ expect((0, utils_1.calculateCurrentWeekStreak)(workouts, firstWeekday, untilUnix)).toBe(4);
722
+ });
723
+ it('Includes this week in the streak count if the user worked out this week', () => {
724
+ const workouts = [
725
+ { start_time: (0, dayjs_1.default)('2025-11-05').unix() },
726
+ { start_time: (0, dayjs_1.default)('2025-10-31').unix() },
727
+ { start_time: (0, dayjs_1.default)('2025-10-23').unix() },
728
+ { start_time: (0, dayjs_1.default)('2025-10-20').unix() },
729
+ { start_time: (0, dayjs_1.default)('2025-10-14').unix() },
730
+ { start_time: (0, dayjs_1.default)('2025-10-07').unix() },
731
+ ];
732
+ const firstWeekday = 'monday';
733
+ const untilUnix = (0, dayjs_1.default)('2025-11-06').unix();
734
+ expect((0, utils_1.calculateCurrentWeekStreak)(workouts, firstWeekday, untilUnix)).toBe(5);
735
+ });
736
+ it("Returns 0 if the user hasn't worked out", () => {
737
+ const workouts = [];
738
+ const firstWeekday = 'friday';
739
+ const untilUnix = (0, dayjs_1.default)('2025-10-10').unix();
740
+ expect((0, utils_1.calculateCurrentWeekStreak)(workouts, firstWeekday, untilUnix)).toBe(0);
741
+ });
742
+ it('Returns 0 if the user has not worked out this week and last week', () => {
743
+ const workouts = [
744
+ { start_time: (0, dayjs_1.default)('2025-10-23').unix() },
745
+ { start_time: (0, dayjs_1.default)('2025-10-20').unix() },
746
+ { start_time: (0, dayjs_1.default)('2025-10-13').unix() },
747
+ { start_time: (0, dayjs_1.default)('2025-10-07').unix() },
748
+ ];
749
+ const firstWeekday = 'sunday';
750
+ const untilUnix = (0, dayjs_1.default)('2025-11-05').unix();
751
+ expect((0, utils_1.calculateCurrentWeekStreak)(workouts, firstWeekday, untilUnix)).toBe(0);
752
+ });
753
+ });
754
+ describe('formatSetValue', () => {
755
+ const baseParams = {
756
+ set: {
757
+ weight_kg: 100,
758
+ reps: 10,
759
+ duration_seconds: 90,
760
+ distance_meters: 1500,
761
+ custom_metric: 15,
762
+ },
763
+ units: {
764
+ weight: 'kg',
765
+ distance: 'kilometers',
766
+ },
767
+ lokalizedLabels: {
768
+ kg: 'kg',
769
+ lbs: 'lbs',
770
+ km: 'km',
771
+ mi: 'mi',
772
+ m: 'm',
773
+ yd: 'yd',
774
+ steps: 'Steps',
775
+ floors: 'Floors',
776
+ },
777
+ };
778
+ it('formats weight and reps for weight-based exercises', () => {
779
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'weight_reps' }))).toBe('100 kg x 10');
780
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'bodyweight_reps' }))).toBe('100 kg x 10');
781
+ });
782
+ it('formats assisted bodyweight reps with negative weight', () => {
783
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'bodyweight_assisted_reps', set: Object.assign(Object.assign({}, baseParams.set), { weight_kg: 15, reps: 8 }) }))).toBe('-15 kg x 8');
784
+ });
785
+ it('formats reps-only exercises', () => {
786
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'reps_only' }))).toBe('10');
787
+ });
788
+ it('formats distance with duration', () => {
789
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'distance_duration' }))).toBe('1.5 km - 1min 30s');
790
+ });
791
+ it('formats duration-only exercises', () => {
792
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'duration' }))).toBe('1min 30s');
793
+ });
794
+ it('formats short distance with weight', () => {
795
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'short_distance_weight', set: Object.assign(Object.assign({}, baseParams.set), { distance_meters: 250 }) }))).toBe('100 kg - 250 m');
796
+ });
797
+ it('handles null reps as zero', () => {
798
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'weight_reps', set: Object.assign(Object.assign({}, baseParams.set), { reps: null }) }))).toBe('100 kg x 0');
799
+ });
800
+ it('formats weight with duration', () => {
801
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'weight_duration' }))).toBe('100 kg - 1min 30s');
802
+ });
803
+ it('formats weights in kg', () => {
804
+ expect((0, utils_1.formatSetValue)({
805
+ set: {
806
+ weight_kg: 100,
807
+ reps: 12,
808
+ duration_seconds: null,
809
+ distance_meters: null,
810
+ custom_metric: null,
811
+ },
812
+ units: {
813
+ weight: 'kg',
814
+ distance: 'kilometers',
815
+ },
816
+ lokalizedLabels: {
817
+ kg: 'kg',
818
+ lbs: 'lbs',
819
+ km: 'km',
820
+ mi: 'mi',
821
+ m: 'm',
822
+ yd: 'yd',
823
+ steps: 'Steps',
824
+ floors: 'Floors',
825
+ },
826
+ exerciseType: 'weight_reps',
827
+ })).toBe('100 kg x 12');
828
+ });
829
+ it('formats weights logged in lbs', () => {
830
+ expect((0, utils_1.formatSetValue)({
831
+ set: {
832
+ weight_kg: 50,
833
+ reps: 12,
834
+ duration_seconds: null,
835
+ distance_meters: null,
836
+ custom_metric: null,
837
+ },
838
+ units: {
839
+ weight: 'lbs',
840
+ distance: 'kilometers',
841
+ },
842
+ lokalizedLabels: {
843
+ kg: 'kg',
844
+ lbs: 'lbs',
845
+ km: 'km',
846
+ mi: 'mi',
847
+ m: 'm',
848
+ yd: 'yd',
849
+ steps: 'Steps',
850
+ floors: 'Floors',
851
+ },
852
+ exerciseType: 'weight_reps',
853
+ })).toBe('110.23 lbs x 12');
854
+ });
855
+ it('formats floors with duration', () => {
856
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'floors_duration' }))).toBe('15 Floors - 1min 30s');
857
+ });
858
+ it('formats steps with duration', () => {
859
+ expect((0, utils_1.formatSetValue)(Object.assign(Object.assign({}, baseParams), { exerciseType: 'steps_duration' }))).toBe('15 Steps - 1min 30s');
860
+ });
861
+ });
862
+ describe('distance', () => {
863
+ it('converts meters to kilometers and rounds to two decimals', () => {
864
+ expect((0, utils_1.distance)(1555, 'kilometers')).toBe(1.56);
865
+ });
866
+ it('converts meters to miles and rounds to two decimals', () => {
867
+ expect((0, utils_1.distance)(1000, 'miles')).toBe(0.62);
868
+ });
869
+ });
870
+ describe('exerciseWeight', () => {
871
+ it('returns rounded kg values for metric units', () => {
872
+ expect((0, utils_1.exerciseWeight)(10.555, 'kg')).toBe(10.56);
873
+ });
874
+ it('converts kg to lbs and rounds to two decimals', () => {
875
+ expect((0, utils_1.exerciseWeight)(100, 'lbs')).toBe(220.46);
876
+ });
877
+ it('rounds lbs values to two decimals', () => {
878
+ expect((0, utils_1.exerciseWeight)(4.535, 'lbs')).toBe(10.0);
879
+ });
880
+ });
133
881
  });