hevy-shared 1.0.1047 → 1.0.1049
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/built/geography.d.ts +1 -0
- package/built/geography.js +15 -0
- package/built/hevyTrainer.d.ts +34 -43
- package/built/hevyTrainer.js +64 -65
- package/built/index.d.ts +115 -7
- package/built/index.js +2 -0
- package/built/tests/geography.test.d.ts +1 -0
- package/built/tests/geography.test.js +25 -0
- package/built/tests/hevyTrainer.test.js +62 -75
- package/built/tests/testUtils.js +1 -0
- package/built/workout/index.d.ts +3 -3
- package/built/workout/index.js +2 -3
- package/built/workout/normalizedWorkout.d.ts +1 -0
- package/built/workout/postWorkoutRequest.d.ts +1 -0
- package/built/workout/publicWorkout.d.ts +1 -3
- package/built/workout/userWorkout.d.ts +1 -28
- package/built/workout/workout.d.ts +2 -36
- package/package.json +1 -1
package/built/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InstructionsLanguage } from './exerciseLocaleUtils';
|
|
2
|
-
import { WorkoutDurationMinutes } from './hevyTrainer';
|
|
2
|
+
import { TrainerWorkoutTemplateName, WorkoutDurationMinutes } from './hevyTrainer';
|
|
3
3
|
import { BugReportQuestionSchema, CreateBugReportRequestBodySchema, postEmailBackofficeSchema } from './schemas';
|
|
4
4
|
import { Language } from './translations';
|
|
5
5
|
import { Lookup } from './typeUtils';
|
|
@@ -29,6 +29,7 @@ export * from './hevyTrainer';
|
|
|
29
29
|
export * from './translations';
|
|
30
30
|
export * from './exerciseLocaleUtils';
|
|
31
31
|
export * from './getVolumeComparison';
|
|
32
|
+
export * from './geography';
|
|
32
33
|
export type WeightUnit = 'kg' | 'lbs';
|
|
33
34
|
export declare const isWeightUnit: (x: string) => x is WeightUnit;
|
|
34
35
|
export type DistanceUnit = 'kilometers' | 'miles';
|
|
@@ -302,8 +303,7 @@ export interface BackofficeExistingUserResponse {
|
|
|
302
303
|
public_api_key: string | null;
|
|
303
304
|
limited_discovery: boolean;
|
|
304
305
|
coach_trial_expire_date?: string;
|
|
305
|
-
hevy_trainer_program:
|
|
306
|
-
email_consent: boolean;
|
|
306
|
+
hevy_trainer_program: TrainerProgramV3 | null;
|
|
307
307
|
}
|
|
308
308
|
export interface BackofficeDeletedUserResponse {
|
|
309
309
|
state: 'deleted-account';
|
|
@@ -748,6 +748,10 @@ export interface RepRange {
|
|
|
748
748
|
start: number | null;
|
|
749
749
|
end: number | null;
|
|
750
750
|
}
|
|
751
|
+
export interface StrictRepRange extends RepRange {
|
|
752
|
+
start: number;
|
|
753
|
+
end: number;
|
|
754
|
+
}
|
|
751
755
|
export interface WorkoutComment {
|
|
752
756
|
id: number;
|
|
753
757
|
username: string;
|
|
@@ -876,12 +880,14 @@ export interface Routine extends BaseRoutine {
|
|
|
876
880
|
username: string;
|
|
877
881
|
coach_id: null;
|
|
878
882
|
}
|
|
883
|
+
/** @deprecated Use TrainerWorkoutTemplate instead */
|
|
879
884
|
export interface HevyTrainerRoutine extends Routine {
|
|
880
885
|
hevy_trainer_program_id: string;
|
|
881
886
|
program_id: null;
|
|
882
887
|
coach_force_rpe_enabled: false;
|
|
883
888
|
folder_id: null;
|
|
884
889
|
}
|
|
890
|
+
/** @deprecated Use TrainerWorkoutTemplate instead */
|
|
885
891
|
export declare const isHevyTrainerRoutine: (routine: BaseRoutine) => routine is HevyTrainerRoutine;
|
|
886
892
|
export type CoachsShallowLibraryRoutine = Omit<BaseRoutine, 'exercises' | 'profile_pic' | 'hevy_trainer_program_id' | 'username'>;
|
|
887
893
|
export interface CoachesRoutine extends BaseRoutine {
|
|
@@ -1050,6 +1056,105 @@ export interface GetTeamInviteResponse {
|
|
|
1050
1056
|
export interface OutstandingInvitesForCoachTeamResponse {
|
|
1051
1057
|
invites: CoachTeamInvite[];
|
|
1052
1058
|
}
|
|
1059
|
+
export interface TrainerWorkoutTemplateResistanceExercise {
|
|
1060
|
+
kind: 'resistance';
|
|
1061
|
+
id: string;
|
|
1062
|
+
exercise_template_id: string;
|
|
1063
|
+
index: number;
|
|
1064
|
+
set_count: number;
|
|
1065
|
+
warmup_set_count: number;
|
|
1066
|
+
rep_range: StrictRepRange;
|
|
1067
|
+
rest_seconds: number;
|
|
1068
|
+
}
|
|
1069
|
+
export interface TrainerWorkoutTemplateCardioExercise {
|
|
1070
|
+
kind: 'cardio';
|
|
1071
|
+
id: string;
|
|
1072
|
+
exercise_template_id: string;
|
|
1073
|
+
index: number;
|
|
1074
|
+
set_count: 1;
|
|
1075
|
+
duration_seconds: number;
|
|
1076
|
+
}
|
|
1077
|
+
export interface TrainerWorkoutTemplateOtherExercise {
|
|
1078
|
+
kind: 'other';
|
|
1079
|
+
id: string;
|
|
1080
|
+
exercise_template_id: string;
|
|
1081
|
+
index: number;
|
|
1082
|
+
set_count: number;
|
|
1083
|
+
}
|
|
1084
|
+
export type TrainerWorkoutTemplateExercise = TrainerWorkoutTemplateResistanceExercise | TrainerWorkoutTemplateCardioExercise | TrainerWorkoutTemplateOtherExercise;
|
|
1085
|
+
export interface TrainerWorkoutTemplate {
|
|
1086
|
+
id: string;
|
|
1087
|
+
hevy_trainer_program_id: string;
|
|
1088
|
+
index: number;
|
|
1089
|
+
name: TrainerWorkoutTemplateName;
|
|
1090
|
+
exercises: TrainerWorkoutTemplateExercise[];
|
|
1091
|
+
}
|
|
1092
|
+
export interface TrainerProgramV3 {
|
|
1093
|
+
id: string;
|
|
1094
|
+
schema_version: 'v3';
|
|
1095
|
+
created_at: string;
|
|
1096
|
+
updated_at: string;
|
|
1097
|
+
level: TrainingLevel;
|
|
1098
|
+
goal: TrainingGoal;
|
|
1099
|
+
equipments: GranularEquipment[];
|
|
1100
|
+
weekly_frequency: WeeklyTrainingFrequency;
|
|
1101
|
+
templates: TrainerWorkoutTemplate[];
|
|
1102
|
+
focus_muscle?: SimplifiedMuscleGroup;
|
|
1103
|
+
next_workout_index: number;
|
|
1104
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1105
|
+
rest_timer_length: RestTimerLength;
|
|
1106
|
+
cardio_preference: CardioPreference;
|
|
1107
|
+
}
|
|
1108
|
+
export type TrainerWorkoutTemplateExerciseInput = Omit<TrainerWorkoutTemplateResistanceExercise, 'id'> | Omit<TrainerWorkoutTemplateCardioExercise, 'id'> | Omit<TrainerWorkoutTemplateOtherExercise, 'id'>;
|
|
1109
|
+
export interface PostTrainerWorkoutTemplate {
|
|
1110
|
+
name: TrainerWorkoutTemplateName;
|
|
1111
|
+
index: number;
|
|
1112
|
+
exercises: TrainerWorkoutTemplateExerciseInput[];
|
|
1113
|
+
}
|
|
1114
|
+
export interface UpdateTrainerWorkoutTemplate extends PostTrainerWorkoutTemplate {
|
|
1115
|
+
id: string;
|
|
1116
|
+
}
|
|
1117
|
+
export interface PostTrainerProgramV3RequestBody {
|
|
1118
|
+
program: {
|
|
1119
|
+
version: 3;
|
|
1120
|
+
title: string;
|
|
1121
|
+
level: TrainingLevel;
|
|
1122
|
+
goal: TrainingGoal;
|
|
1123
|
+
equipments: GranularEquipment[];
|
|
1124
|
+
weekly_frequency: WeeklyTrainingFrequency;
|
|
1125
|
+
focus_muscle?: SimplifiedMuscleGroup;
|
|
1126
|
+
next_workout_index?: number;
|
|
1127
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1128
|
+
rest_timer_length: RestTimerLength;
|
|
1129
|
+
cardio_preference: CardioPreference;
|
|
1130
|
+
templates: PostTrainerWorkoutTemplate[];
|
|
1131
|
+
};
|
|
1132
|
+
/**
|
|
1133
|
+
* Whether to delete (archive) the caller's currently active Hevy Trainer
|
|
1134
|
+
* program before creating the new one. Creating a program implicitly
|
|
1135
|
+
* replaces any active program, so this must be set explicitly:
|
|
1136
|
+
* - `false` — fail with `ActiveProgramExists` (409) if one already exists.
|
|
1137
|
+
* - `true` — archive the existing active program, then create the new one.
|
|
1138
|
+
*/
|
|
1139
|
+
deleteActiveProgram: boolean;
|
|
1140
|
+
}
|
|
1141
|
+
export interface UpdateTrainerProgramV3RequestBody {
|
|
1142
|
+
program: {
|
|
1143
|
+
version: 3;
|
|
1144
|
+
programId: string;
|
|
1145
|
+
level: TrainingLevel;
|
|
1146
|
+
goal: TrainingGoal;
|
|
1147
|
+
equipments: GranularEquipment[];
|
|
1148
|
+
weekly_frequency: WeeklyTrainingFrequency;
|
|
1149
|
+
focus_muscle?: SimplifiedMuscleGroup;
|
|
1150
|
+
next_workout_index?: number;
|
|
1151
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1152
|
+
rest_timer_length: RestTimerLength;
|
|
1153
|
+
cardio_preference: CardioPreference;
|
|
1154
|
+
templates: UpdateTrainerWorkoutTemplate[];
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/** @deprecated Use TrainerProgramV3 instead */
|
|
1053
1158
|
export interface HevyTrainerProgram {
|
|
1054
1159
|
id: string;
|
|
1055
1160
|
created_at: string;
|
|
@@ -1066,6 +1171,7 @@ export interface HevyTrainerProgram {
|
|
|
1066
1171
|
rest_timer_length: RestTimerLength;
|
|
1067
1172
|
cardio_preference: CardioPreference;
|
|
1068
1173
|
}
|
|
1174
|
+
/** @deprecated Use PostTrainerProgramV3RequestBody instead */
|
|
1069
1175
|
export interface PostHevyTrainerProgramRequestBody {
|
|
1070
1176
|
program: {
|
|
1071
1177
|
version: 1;
|
|
@@ -1082,6 +1188,7 @@ export interface PostHevyTrainerProgramRequestBody {
|
|
|
1082
1188
|
cardio_preference: CardioPreference;
|
|
1083
1189
|
};
|
|
1084
1190
|
}
|
|
1191
|
+
/** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
|
|
1085
1192
|
export interface UpdateHevyTrainerProgramRequestBody {
|
|
1086
1193
|
program: {
|
|
1087
1194
|
version: 1;
|
|
@@ -1104,7 +1211,7 @@ export interface UpdateHevyTrainerProgramRequestBody {
|
|
|
1104
1211
|
}[];
|
|
1105
1212
|
};
|
|
1106
1213
|
}
|
|
1107
|
-
/** @deprecated Use
|
|
1214
|
+
/** @deprecated Use TrainerProgramV3 instead */
|
|
1108
1215
|
export interface HevyTrainerProgramOld {
|
|
1109
1216
|
id: string;
|
|
1110
1217
|
created_at: string;
|
|
@@ -1119,7 +1226,7 @@ export interface HevyTrainerProgramOld {
|
|
|
1119
1226
|
next_workout_index: number;
|
|
1120
1227
|
workout_duration_minutes?: WorkoutDurationMinutes;
|
|
1121
1228
|
}
|
|
1122
|
-
/** @deprecated Use
|
|
1229
|
+
/** @deprecated Use PostTrainerProgramV3RequestBody instead */
|
|
1123
1230
|
export interface PostHevyTrainerProgramOldRequestBody {
|
|
1124
1231
|
program: {
|
|
1125
1232
|
title: string;
|
|
@@ -1133,7 +1240,7 @@ export interface PostHevyTrainerProgramOldRequestBody {
|
|
|
1133
1240
|
workout_duration_minutes?: WorkoutDurationMinutes;
|
|
1134
1241
|
};
|
|
1135
1242
|
}
|
|
1136
|
-
/** @deprecated Use
|
|
1243
|
+
/** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
|
|
1137
1244
|
export interface UpdateHevyTrainerProgramOldRequestBody {
|
|
1138
1245
|
program: {
|
|
1139
1246
|
programId: string;
|
|
@@ -1407,7 +1514,8 @@ type CommercialGym = {
|
|
|
1407
1514
|
name: string;
|
|
1408
1515
|
fullAddress: string;
|
|
1409
1516
|
city: string;
|
|
1410
|
-
|
|
1517
|
+
latitude: number;
|
|
1518
|
+
longitude: number;
|
|
1411
1519
|
};
|
|
1412
1520
|
export type Gym = CommercialGym;
|
|
1413
1521
|
export interface StripePrice {
|
package/built/index.js
CHANGED
|
@@ -41,6 +41,7 @@ __exportStar(require("./hevyTrainer"), exports);
|
|
|
41
41
|
__exportStar(require("./translations"), exports);
|
|
42
42
|
__exportStar(require("./exerciseLocaleUtils"), exports);
|
|
43
43
|
__exportStar(require("./getVolumeComparison"), exports);
|
|
44
|
+
__exportStar(require("./geography"), exports);
|
|
44
45
|
const isWeightUnit = (x) => {
|
|
45
46
|
return x === 'kg' || x === 'lbs';
|
|
46
47
|
};
|
|
@@ -321,6 +322,7 @@ const isWorkoutBiometrics = (x) => {
|
|
|
321
322
|
return caloriesAreValid && heartSamplesAreValid;
|
|
322
323
|
};
|
|
323
324
|
exports.isWorkoutBiometrics = isWorkoutBiometrics;
|
|
325
|
+
/** @deprecated Use TrainerWorkoutTemplate instead */
|
|
324
326
|
const isHevyTrainerRoutine = (routine) => routine.hevy_trainer_program_id !== null;
|
|
325
327
|
exports.isHevyTrainerRoutine = isHevyTrainerRoutine;
|
|
326
328
|
exports.measurementsList = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const geography_1 = require("../geography");
|
|
13
|
+
describe('Geography utils', () => {
|
|
14
|
+
describe('distanceMBetween', () => {
|
|
15
|
+
it('Calculates the distance between two sets of coordinates', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
16
|
+
expect((0, geography_1.distanceMBetween)(51.001, 52, 51, 52)).toBe(111);
|
|
17
|
+
expect((0, geography_1.distanceMBetween)(51.002, 52, 51, 52)).toBe(222);
|
|
18
|
+
expect((0, geography_1.distanceMBetween)(50.999, 52, 51, 52)).toBe(111);
|
|
19
|
+
expect((0, geography_1.distanceMBetween)(51.01, 52, 51, 52)).toBe(1112);
|
|
20
|
+
expect((0, geography_1.distanceMBetween)(51.04, 52, 51, 52)).toBe(4448);
|
|
21
|
+
expect((0, geography_1.distanceMBetween)(51.035, 52.035, 51, 52)).toBe(4598);
|
|
22
|
+
expect((0, geography_1.distanceMBetween)(50.965, 51.965, 51, 52)).toBe(4599);
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -236,7 +236,7 @@ const makeSettings = (overrides = {}) => {
|
|
|
236
236
|
};
|
|
237
237
|
return acc;
|
|
238
238
|
}, {});
|
|
239
|
-
const templates = hevyTrainer_1.
|
|
239
|
+
const templates = hevyTrainer_1.workoutTemplateNames.reduce((acc, name) => {
|
|
240
240
|
acc[name] = { exercises: [] };
|
|
241
241
|
return acc;
|
|
242
242
|
}, {});
|
|
@@ -372,7 +372,7 @@ describe('getPrioritySortedExercises', () => {
|
|
|
372
372
|
expect(result.quadriceps.map((e) => e.id)).toEqual(['leg-ex']);
|
|
373
373
|
});
|
|
374
374
|
});
|
|
375
|
-
const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [],
|
|
375
|
+
const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [], workoutBarbellExerciseCount: 0, level: 'beginner', goal: 'strength', muscleGroup: 'chest', frequency: 3 }, overrides));
|
|
376
376
|
/** Produces a `sortedExercises` record where only `chest` is populated. */
|
|
377
377
|
const chestOnlySorted = (exercises) => {
|
|
378
378
|
const empty = __1.muscleGroups.reduce((acc, mg) => {
|
|
@@ -394,7 +394,7 @@ describe('pickTrainerExercise', () => {
|
|
|
394
394
|
});
|
|
395
395
|
expect(result === null || result === void 0 ? void 0 : result.id).toBe('fresh');
|
|
396
396
|
});
|
|
397
|
-
it('pass 2: falls back to exercises not used in the current
|
|
397
|
+
it('pass 2: falls back to exercises not used in the current workout when all are used in the program', () => {
|
|
398
398
|
const onlyExercise = makeExercise({ id: 'only' });
|
|
399
399
|
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
400
400
|
sortedExercises: chestOnlySorted([onlyExercise]),
|
|
@@ -402,8 +402,8 @@ describe('pickTrainerExercise', () => {
|
|
|
402
402
|
context: {
|
|
403
403
|
// Marked as used in the whole program already.
|
|
404
404
|
programUsedExerciseIds: new Set(['only']),
|
|
405
|
-
// But not used in this
|
|
406
|
-
|
|
405
|
+
// But not used in this workout, so pass 2 should succeed.
|
|
406
|
+
workoutUsedExerciseIds: new Set(),
|
|
407
407
|
},
|
|
408
408
|
});
|
|
409
409
|
expect(result === null || result === void 0 ? void 0 : result.id).toBe('only');
|
|
@@ -506,7 +506,7 @@ describe('pickTrainerExercise', () => {
|
|
|
506
506
|
sortedExercises: chestOnlySorted([ex]),
|
|
507
507
|
criteria: baseCriteria({
|
|
508
508
|
frequency: 3,
|
|
509
|
-
|
|
509
|
+
workoutBarbellExerciseCount: 10,
|
|
510
510
|
equipments: ['dumbbell'],
|
|
511
511
|
}),
|
|
512
512
|
context: {},
|
|
@@ -519,7 +519,7 @@ describe('pickTrainerExercise', () => {
|
|
|
519
519
|
sortedExercises: chestOnlySorted([ex]),
|
|
520
520
|
criteria: baseCriteria({
|
|
521
521
|
frequency: 1,
|
|
522
|
-
|
|
522
|
+
workoutBarbellExerciseCount: 2,
|
|
523
523
|
equipments: ['dumbbell'],
|
|
524
524
|
}),
|
|
525
525
|
context: {},
|
|
@@ -532,7 +532,7 @@ describe('pickTrainerExercise', () => {
|
|
|
532
532
|
sortedExercises: chestOnlySorted([ex]),
|
|
533
533
|
criteria: baseCriteria({
|
|
534
534
|
frequency: 1,
|
|
535
|
-
|
|
535
|
+
workoutBarbellExerciseCount: 3,
|
|
536
536
|
// Dumbbell is listed as a barbell substitute, so the cap applies.
|
|
537
537
|
equipments: ['dumbbell'],
|
|
538
538
|
}),
|
|
@@ -546,7 +546,7 @@ describe('pickTrainerExercise', () => {
|
|
|
546
546
|
sortedExercises: chestOnlySorted([ex]),
|
|
547
547
|
criteria: baseCriteria({
|
|
548
548
|
frequency: 1,
|
|
549
|
-
|
|
549
|
+
workoutBarbellExerciseCount: 5,
|
|
550
550
|
// No substitutes → avoid dead-ends by allowing the barbell.
|
|
551
551
|
equipments: [],
|
|
552
552
|
}),
|
|
@@ -587,7 +587,7 @@ describe('pickTrainerExercise', () => {
|
|
|
587
587
|
describe('generateProgram', () => {
|
|
588
588
|
const buildSettingsForChestProgram = () => {
|
|
589
589
|
const settings = makeSettings();
|
|
590
|
-
// Put a single chest compound prescription into the 1-day full body
|
|
590
|
+
// Put a single chest compound prescription into the 1-day full body workout.
|
|
591
591
|
settings.templates.full_body_1 = {
|
|
592
592
|
exercises: [
|
|
593
593
|
{ muscle_group: 'chest', category: 'compound', warmup_set_count: 2 },
|
|
@@ -596,7 +596,7 @@ describe('generateProgram', () => {
|
|
|
596
596
|
};
|
|
597
597
|
return settings;
|
|
598
598
|
};
|
|
599
|
-
it('produces one
|
|
599
|
+
it('produces one workout per programSplit and populates exercise fields from the settings', () => {
|
|
600
600
|
const settings = buildSettingsForChestProgram();
|
|
601
601
|
const chestExercise = makeExercise({ id: 'bench', muscle_group: 'chest' });
|
|
602
602
|
const result = (0, hevyTrainer_1.generateProgram)({
|
|
@@ -613,27 +613,19 @@ describe('generateProgram', () => {
|
|
|
613
613
|
expect(result.success).toBe(true);
|
|
614
614
|
if (!result.success)
|
|
615
615
|
return;
|
|
616
|
-
expect(result.program.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
expect(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const [exercise] = routine.exercises;
|
|
623
|
-
expect(exercise.kind).toBe('resistance');
|
|
624
|
-
if (exercise.kind !== 'resistance')
|
|
616
|
+
expect(result.program.workoutTemplates).toHaveLength(hevyTrainer_1.programSplits[1].length);
|
|
617
|
+
const [workoutTemplate] = result.program.workoutTemplates;
|
|
618
|
+
expect(workoutTemplate.name).toBe('full_body_1');
|
|
619
|
+
expect(workoutTemplate.exercises).toHaveLength(1);
|
|
620
|
+
const [exercise] = workoutTemplate.exercises;
|
|
621
|
+
if (!(0, hevyTrainer_1.isTrainerProgramResistanceExercise)(exercise))
|
|
625
622
|
return;
|
|
626
|
-
expect(exercise.
|
|
627
|
-
expect(exercise.
|
|
628
|
-
expect(exercise.
|
|
629
|
-
expect(exercise.
|
|
630
|
-
|
|
631
|
-
expect(exercise.
|
|
632
|
-
// strength + compound -> 1..5
|
|
633
|
-
expect(exercise.repRangeStart).toBe(1);
|
|
634
|
-
expect(exercise.repRangeEnd).toBe(5);
|
|
635
|
-
// strength + medium + compound -> 60
|
|
636
|
-
expect(exercise.restTimerSeconds).toBe(60);
|
|
623
|
+
expect(exercise.kind).toBe('resistance');
|
|
624
|
+
expect(exercise.exercise_template.id).toBe('bench');
|
|
625
|
+
expect(exercise.warmup_set_count).toBe(2);
|
|
626
|
+
expect(exercise.set_count).toBe(1);
|
|
627
|
+
expect(exercise.rep_range).toEqual({ start: 1, end: 5 });
|
|
628
|
+
expect(exercise.rest_seconds).toBe(60);
|
|
637
629
|
});
|
|
638
630
|
it('returns a partial program and errors when no matching exercise can be found', () => {
|
|
639
631
|
const settings = buildSettingsForChestProgram();
|
|
@@ -656,14 +648,14 @@ describe('generateProgram', () => {
|
|
|
656
648
|
type: 'exercise_not_found',
|
|
657
649
|
muscleGroup: 'chest',
|
|
658
650
|
});
|
|
659
|
-
// The
|
|
651
|
+
// The workout template structure still exists in the partial program, just without
|
|
660
652
|
// any resolved exercises.
|
|
661
|
-
expect(result.partialProgram.
|
|
662
|
-
expect(result.partialProgram.
|
|
653
|
+
expect(result.partialProgram.workoutTemplates).toHaveLength(1);
|
|
654
|
+
expect(result.partialProgram.workoutTemplates[0].exercises).toEqual([]);
|
|
663
655
|
});
|
|
664
656
|
describe('min_workout_duration_limit filtering', () => {
|
|
665
657
|
/**
|
|
666
|
-
* Runs a 1-day program containing a single chest compound prescription
|
|
658
|
+
* Runs a 1-day workout program containing a single chest compound prescription
|
|
667
659
|
* with the provided `min_workout_duration_limit` against the provided
|
|
668
660
|
* `workoutDurationMinutes`, and returns whether the prescription was
|
|
669
661
|
* placed.
|
|
@@ -690,9 +682,8 @@ describe('generateProgram', () => {
|
|
|
690
682
|
exerciseStore: [makeExercise({ id: 'bench' })],
|
|
691
683
|
cardioPreference: 'no-cardio',
|
|
692
684
|
});
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
return routine.exercises.length === 1;
|
|
685
|
+
const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates[0];
|
|
686
|
+
return workoutTemplate.exercises.length === 1;
|
|
696
687
|
};
|
|
697
688
|
it('skips prescriptions whose limit is strictly greater than the selected duration', () => {
|
|
698
689
|
expect(runDurationProbe({ limit: 80, workoutDurationMinutes: 40 })).toBe(false);
|
|
@@ -764,11 +755,11 @@ describe('generateProgram', () => {
|
|
|
764
755
|
return;
|
|
765
756
|
// Only the 40- and 60-minute prescriptions survive; the 80-minute one
|
|
766
757
|
// is filtered out. Order is preserved.
|
|
767
|
-
const placed = result.program.
|
|
758
|
+
const placed = result.program.workoutTemplates[0].exercises;
|
|
768
759
|
expect(placed.every((e) => e.kind === 'resistance')).toBe(true);
|
|
769
760
|
expect(placed
|
|
770
761
|
.filter(hevyTrainer_1.isTrainerProgramResistanceExercise)
|
|
771
|
-
.map((e) => e.
|
|
762
|
+
.map((e) => e.warmup_set_count)).toEqual([1, 2]);
|
|
772
763
|
});
|
|
773
764
|
});
|
|
774
765
|
it('skips focus_muscle prescriptions when no focusMuscle is provided', () => {
|
|
@@ -792,7 +783,7 @@ describe('generateProgram', () => {
|
|
|
792
783
|
expect(result.success).toBe(true);
|
|
793
784
|
if (!result.success)
|
|
794
785
|
return;
|
|
795
|
-
expect(result.program.
|
|
786
|
+
expect(result.program.workoutTemplates[0].exercises).toEqual([]);
|
|
796
787
|
});
|
|
797
788
|
it('routes focus_muscle prescriptions to the requested SimplifiedMuscleGroup', () => {
|
|
798
789
|
const settings = makeSettings();
|
|
@@ -819,12 +810,11 @@ describe('generateProgram', () => {
|
|
|
819
810
|
expect(result.success).toBe(true);
|
|
820
811
|
if (!result.success)
|
|
821
812
|
return;
|
|
822
|
-
const [exercise] = result.program.
|
|
813
|
+
const [exercise] = result.program.workoutTemplates[0].exercises;
|
|
823
814
|
expect(exercise.kind).toBe('resistance');
|
|
824
815
|
if (exercise.kind !== 'resistance')
|
|
825
816
|
return;
|
|
826
|
-
expect(exercise.
|
|
827
|
-
expect(exercise.muscleGroup).toBe('focus_muscle');
|
|
817
|
+
expect(exercise.exercise_template.id).toBe('biceps-curl');
|
|
828
818
|
});
|
|
829
819
|
it('excludes exercises listed in `excludedExerciseIds`', () => {
|
|
830
820
|
const settings = buildSettingsForChestProgram();
|
|
@@ -845,7 +835,7 @@ describe('generateProgram', () => {
|
|
|
845
835
|
expect(result.success).toBe(true);
|
|
846
836
|
if (!result.success)
|
|
847
837
|
return;
|
|
848
|
-
expect(result.program.
|
|
838
|
+
expect(result.program.workoutTemplates[0].exercises[0].exercise_template.id).toBe('alt');
|
|
849
839
|
});
|
|
850
840
|
it('attaches exercise selection traces when debugExerciseSelectionTrace is true', () => {
|
|
851
841
|
const settings = buildSettingsForChestProgram();
|
|
@@ -866,7 +856,7 @@ describe('generateProgram', () => {
|
|
|
866
856
|
expect(result.exerciseSelectionTraces).toHaveLength(1);
|
|
867
857
|
const [trace] = result.exerciseSelectionTraces;
|
|
868
858
|
expect(trace.traceKind).toBe('template');
|
|
869
|
-
expect(trace.
|
|
859
|
+
expect(trace.workoutTemplate).toBe('full_body_1');
|
|
870
860
|
expect(trace.selectedExerciseId).toBe('bench');
|
|
871
861
|
expect(trace.trace.selectedPass).toBe(1);
|
|
872
862
|
});
|
|
@@ -938,7 +928,7 @@ describe('generateProgram', () => {
|
|
|
938
928
|
expect(cardioTrace.equipments).toEqual([]);
|
|
939
929
|
expect(cardioTrace.level).toBe('beginner');
|
|
940
930
|
});
|
|
941
|
-
it('uses the same prioritized cardio exercise on every
|
|
931
|
+
it('uses the same prioritized cardio exercise on every workout (not blocked by program reuse)', () => {
|
|
942
932
|
const settings = makeSettings();
|
|
943
933
|
settings.templates.full_body_2_a = {
|
|
944
934
|
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
@@ -971,13 +961,13 @@ describe('generateProgram', () => {
|
|
|
971
961
|
expect(result.success).toBe(true);
|
|
972
962
|
if (!result.success)
|
|
973
963
|
return;
|
|
974
|
-
expect(result.program.
|
|
964
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
975
965
|
kind: 'cardio',
|
|
976
|
-
|
|
966
|
+
exercise_template: { id: 'run' },
|
|
977
967
|
});
|
|
978
|
-
expect(result.program.
|
|
968
|
+
expect(result.program.workoutTemplates[1].exercises[0]).toMatchObject({
|
|
979
969
|
kind: 'cardio',
|
|
980
|
-
|
|
970
|
+
exercise_template: { id: 'run' },
|
|
981
971
|
});
|
|
982
972
|
});
|
|
983
973
|
it('skips cardio exercises that need equipment the user does not have', () => {
|
|
@@ -1016,9 +1006,9 @@ describe('generateProgram', () => {
|
|
|
1016
1006
|
expect(result.success).toBe(true);
|
|
1017
1007
|
if (!result.success)
|
|
1018
1008
|
return;
|
|
1019
|
-
expect(result.program.
|
|
1009
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
1020
1010
|
kind: 'cardio',
|
|
1021
|
-
|
|
1011
|
+
exercise_template: { id: 'body-run' },
|
|
1022
1012
|
});
|
|
1023
1013
|
});
|
|
1024
1014
|
it('skips globally excluded cardio and uses the next priority exercise', () => {
|
|
@@ -1057,12 +1047,12 @@ describe('generateProgram', () => {
|
|
|
1057
1047
|
expect(result.success).toBe(true);
|
|
1058
1048
|
if (!result.success)
|
|
1059
1049
|
return;
|
|
1060
|
-
expect(result.program.
|
|
1050
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
1061
1051
|
kind: 'cardio',
|
|
1062
|
-
|
|
1052
|
+
exercise_template: { id: 'run-b' },
|
|
1063
1053
|
});
|
|
1064
1054
|
});
|
|
1065
|
-
it('keeps global trace order aligned with
|
|
1055
|
+
it('keeps global trace order aligned with workout generation (no cross-workout unshift)', () => {
|
|
1066
1056
|
const settings = makeSettings();
|
|
1067
1057
|
settings.templates.full_body_2_a = {
|
|
1068
1058
|
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
@@ -1096,15 +1086,15 @@ describe('generateProgram', () => {
|
|
|
1096
1086
|
expect(result.success).toBe(true);
|
|
1097
1087
|
if (!result.success)
|
|
1098
1088
|
return;
|
|
1099
|
-
const idxA = result.exerciseSelectionTraces.findIndex((t) => t.
|
|
1100
|
-
const idxB = result.exerciseSelectionTraces.findIndex((t) => t.
|
|
1089
|
+
const idxA = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_a');
|
|
1090
|
+
const idxB = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_b');
|
|
1101
1091
|
expect(idxA).toBeGreaterThanOrEqual(0);
|
|
1102
1092
|
expect(idxB).toBeGreaterThan(idxA);
|
|
1103
1093
|
});
|
|
1104
1094
|
describe('barbell cap at frequency === 1 (end-to-end)', () => {
|
|
1105
1095
|
it('drops the 4th+ barbell prescription when the user has a substitute', () => {
|
|
1106
1096
|
const settings = makeSettings();
|
|
1107
|
-
// 4 identical barbell compound prescriptions in the 1-day
|
|
1097
|
+
// 4 identical barbell compound prescriptions in the 1-day workout.
|
|
1108
1098
|
settings.templates.full_body_1 = {
|
|
1109
1099
|
exercises: Array.from({ length: 4 }, () => ({
|
|
1110
1100
|
muscle_group: 'chest',
|
|
@@ -1133,7 +1123,7 @@ describe('generateProgram', () => {
|
|
|
1133
1123
|
expect(result.success).toBe(false);
|
|
1134
1124
|
if (result.success)
|
|
1135
1125
|
return;
|
|
1136
|
-
expect(result.partialProgram.
|
|
1126
|
+
expect(result.partialProgram.workoutTemplates[0].exercises).toHaveLength(3);
|
|
1137
1127
|
expect(result.errors).toHaveLength(1);
|
|
1138
1128
|
});
|
|
1139
1129
|
it('allows 4+ barbell prescriptions at frequency 1 when no substitute is present', () => {
|
|
@@ -1164,19 +1154,19 @@ describe('generateProgram', () => {
|
|
|
1164
1154
|
expect(result.success).toBe(true);
|
|
1165
1155
|
if (!result.success)
|
|
1166
1156
|
return;
|
|
1167
|
-
expect(result.program.
|
|
1157
|
+
expect(result.program.workoutTemplates[0].exercises).toHaveLength(4);
|
|
1168
1158
|
});
|
|
1169
1159
|
});
|
|
1170
1160
|
describe('focus_muscle cycling', () => {
|
|
1171
1161
|
it('cycles through simplifiedMuscleGroupToMuscleGroups as focus_muscle prescriptions are placed', () => {
|
|
1172
1162
|
// Use a 2-day split so we can place 4 focus_muscle exercises across two
|
|
1173
|
-
//
|
|
1163
|
+
// workouts and confirm the counter survives between workouts.
|
|
1174
1164
|
const settings = makeSettings();
|
|
1175
1165
|
const focusPrescription = {
|
|
1176
1166
|
muscle_group: 'focus_muscle',
|
|
1177
1167
|
category: 'isolation',
|
|
1178
1168
|
};
|
|
1179
|
-
// full_body
|
|
1169
|
+
// full_body workouts allow any focus muscle. Place 2 per workout so the
|
|
1180
1170
|
// combined count across the program is 4.
|
|
1181
1171
|
settings.templates.full_body_2_a = {
|
|
1182
1172
|
exercises: [focusPrescription, focusPrescription],
|
|
@@ -1246,7 +1236,7 @@ describe('generateProgram', () => {
|
|
|
1246
1236
|
settings.templates.upper_2 = { exercises: [focusPrescription] };
|
|
1247
1237
|
settings.templates.lower_2 = { exercises: [focusPrescription] };
|
|
1248
1238
|
// `legs` cycles through ['quadriceps', 'hamstrings', 'calves', 'glutes',
|
|
1249
|
-
// 'abductors', 'adductors']. The first two allowed
|
|
1239
|
+
// 'abductors', 'adductors']. The first two allowed workouts (legs_1 and
|
|
1250
1240
|
// lower_2) should land on quadriceps and hamstrings respectively.
|
|
1251
1241
|
const exercises = [
|
|
1252
1242
|
makeExercise({
|
|
@@ -1279,10 +1269,7 @@ describe('generateProgram', () => {
|
|
|
1279
1269
|
// Only legs_1 and lower_2 run the focus_muscle prescription. Both
|
|
1280
1270
|
// succeed and should have selected quadriceps then hamstrings in order.
|
|
1281
1271
|
expect(result.exerciseSelectionTraces).toHaveLength(2);
|
|
1282
|
-
expect(result.exerciseSelectionTraces.map((t) => t.
|
|
1283
|
-
'legs_1',
|
|
1284
|
-
'lower_2',
|
|
1285
|
-
]);
|
|
1272
|
+
expect(result.exerciseSelectionTraces.map((t) => t.workoutTemplate)).toEqual(['legs_1', 'lower_2']);
|
|
1286
1273
|
expect(result.exerciseSelectionTraces
|
|
1287
1274
|
.filter((t) => t.traceKind === 'template')
|
|
1288
1275
|
.map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
|
|
@@ -1290,14 +1277,14 @@ describe('generateProgram', () => {
|
|
|
1290
1277
|
});
|
|
1291
1278
|
describe('isFocusMuscleExtraExerciseAllowed template compatibility', () => {
|
|
1292
1279
|
/**
|
|
1293
|
-
* Runs a single-
|
|
1280
|
+
* Runs a single-workout template program that places one focus_muscle
|
|
1294
1281
|
* prescription into the requested template and returns whether the
|
|
1295
1282
|
* prescription produced an exercise or was skipped.
|
|
1296
1283
|
*/
|
|
1297
|
-
const runFocusMuscleProbe = (
|
|
1284
|
+
const runFocusMuscleProbe = (workoutTemplateName, focusMuscle, frequency, candidateExercise) => {
|
|
1298
1285
|
var _a;
|
|
1299
1286
|
const settings = makeSettings();
|
|
1300
|
-
settings.templates[
|
|
1287
|
+
settings.templates[workoutTemplateName] = {
|
|
1301
1288
|
exercises: [{ muscle_group: 'focus_muscle', category: 'isolation' }],
|
|
1302
1289
|
};
|
|
1303
1290
|
const result = (0, hevyTrainer_1.generateProgram)({
|
|
@@ -1313,13 +1300,13 @@ describe('generateProgram', () => {
|
|
|
1313
1300
|
debugExerciseSelectionTrace: true,
|
|
1314
1301
|
cardioPreference: 'no-cardio',
|
|
1315
1302
|
});
|
|
1316
|
-
// Find the
|
|
1317
|
-
const
|
|
1303
|
+
// Find the workout template we targeted (it may not be first in the split).
|
|
1304
|
+
const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates.find((w) => w.name === workoutTemplateName);
|
|
1318
1305
|
return {
|
|
1319
|
-
placed: ((_a =
|
|
1306
|
+
placed: ((_a = workoutTemplate === null || workoutTemplate === void 0 ? void 0 : workoutTemplate.exercises.length) !== null && _a !== void 0 ? _a : 0) > 0,
|
|
1320
1307
|
};
|
|
1321
1308
|
};
|
|
1322
|
-
it('allows every focus muscle on full_body
|
|
1309
|
+
it('allows every focus muscle on full_body workouts', () => {
|
|
1323
1310
|
const muscles = ['chest', 'shoulders', 'arms', 'legs', 'back'];
|
|
1324
1311
|
muscles.forEach((focusMuscle) => {
|
|
1325
1312
|
// Use a muscle that's in the simplified group: biceps for arms, chest
|
package/built/tests/testUtils.js
CHANGED
package/built/workout/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface WorkoutSync {
|
|
|
11
11
|
isMore: boolean;
|
|
12
12
|
updated_at?: string;
|
|
13
13
|
}
|
|
14
|
-
export type Workout = OwnedWorkout
|
|
15
|
-
export type WorkoutExercise = OwnedWorkoutExercise
|
|
16
|
-
export type WorkoutExerciseSet = OwnedWorkoutExerciseSet
|
|
14
|
+
export type Workout = OwnedWorkout | UserWorkout;
|
|
15
|
+
export type WorkoutExercise = OwnedWorkoutExercise | UserWorkoutExercise;
|
|
16
|
+
export type WorkoutExerciseSet = OwnedWorkoutExerciseSet | UserWorkoutExerciseSet;
|
|
17
17
|
export declare function isOwnedWorkout(workout: OwnedWorkout | UserWorkout): workout is OwnedWorkout;
|
package/built/workout/index.js
CHANGED
|
@@ -20,8 +20,7 @@ __exportStar(require("./userWorkout"), exports);
|
|
|
20
20
|
__exportStar(require("./publicWorkout"), exports);
|
|
21
21
|
__exportStar(require("./normalizedWorkout"), exports);
|
|
22
22
|
__exportStar(require("./postWorkoutRequest"), exports);
|
|
23
|
-
//
|
|
23
|
+
// Placeholder, will be done better but this works for now
|
|
24
24
|
function isOwnedWorkout(workout) {
|
|
25
|
-
|
|
26
|
-
return true;
|
|
25
|
+
return !('gym' in workout);
|
|
27
26
|
}
|