running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

WorkoutCalculator.spec.js (11404B)


      1 import { beforeEach, test, expect } from 'vitest';
      2 import { shallowMount } from '@vue/test-utils';
      3 import WorkoutCalculator from '@/views/WorkoutCalculator.vue';
      4 import { defaultTargetSets } from '@/core/targets';
      5 import { detectDefaultUnitSystem } from '@/core/units';
      6 
      7 beforeEach(() => {
      8   localStorage.clear();
      9 });
     10 
     11 test('should initialize options to default values', async () => {
     12   // Initialize component
     13   const wrapper = shallowMount(WorkoutCalculator);
     14 
     15   // Assert options are initialized
     16   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
     17     defaultUnitSystem: detectDefaultUnitSystem(),
     18     racePredictionOptions: {
     19       model: 'AverageModel',
     20       riegelExponent: 1.06,
     21     },
     22   });
     23   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
     24     customTargetNames: false,
     25     input: {
     26       distanceValue: 5,
     27       distanceUnit: 'kilometers',
     28       time: 1200,
     29     },
     30     selectedTargetSet: '_workout_targets',
     31   });
     32   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
     33     .to.deep.equal({ _workout_targets: defaultTargetSets._workout_targets });
     34   expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
     35     .to.deep.equal(defaultTargetSets._workout_targets.targets);
     36 });
     37 
     38 test('should load options from localStorage', async () => {
     39   const targetSets = {
     40     '_workout_targets': {
     41       name: 'Workout targets #1',
     42       targets: [
     43         {
     44           splitValue: 400, splitUnit: 'meters',
     45           type: 'distance', distanceValue: 1, distanceUnit: 'miles',
     46         },
     47         {
     48           splitValue: 800, splitUnit: 'meters',
     49           type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
     50         },
     51         {
     52           splitValue: 1600, splitUnit: 'meters',
     53           type: 'time', time: 3600,
     54         },
     55         {
     56           splitValue: 2, splitUnit: 'miles',
     57           type: 'time', time: 7200,
     58         },
     59       ],
     60     },
     61     'B': {
     62       name: 'Workout targets #2',
     63       targets: [
     64         {
     65           distanceUnit: 'miles', distanceValue: 2,
     66           splitUnit: 'meters', splitValue: 400,
     67           type: 'distance',
     68         },
     69         {
     70           time: 6000,
     71           splitUnit: 'kilometers', splitValue: 2,
     72           type: 'time',
     73         },
     74         {
     75           distanceUnit: 'kilometers', distanceValue: 5,
     76           splitUnit: 'miles', splitValue: 1,
     77           type: 'distance'
     78         },
     79       ],
     80     },
     81   };
     82 
     83   // Initialize localStorage
     84   localStorage.setItem('running-tools.global-options', JSON.stringify({
     85     defaultUnitSystem: 'imperial',
     86     racePredictionOptions: {
     87       model: 'PurdyPointsModel',
     88       riegelExponent: 1.2,
     89     },
     90   }));
     91   localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify(targetSets));
     92   localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
     93     customTargetNames: true,
     94     input: {
     95       distanceValue: 1,
     96       distanceUnit: 'miles',
     97       time: 600,
     98     },
     99     selectedTargetSet: 'B',
    100   }));
    101 
    102   // Initialize component
    103   const wrapper = shallowMount(WorkoutCalculator);
    104 
    105   // Assert options are loaded
    106   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
    107     defaultUnitSystem: 'imperial',
    108     racePredictionOptions: {
    109       model: 'PurdyPointsModel',
    110       riegelExponent: 1.2,
    111     },
    112   });
    113   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
    114     customTargetNames: true,
    115     input: {
    116       distanceValue: 1,
    117       distanceUnit: 'miles',
    118       time: 600,
    119     },
    120     selectedTargetSet: 'B',
    121   });
    122   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
    123     .to.deep.equal(targetSets);
    124   expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
    125     .to.deep.equal(targetSets.B.targets);
    126 });
    127 
    128 test('should save options to localStorage when modified', async () => {
    129   const targetSets = {
    130     '_workout_targets': {
    131       name: 'Workout targets #1',
    132       targets: [
    133         {
    134           splitValue: 400, splitUnit: 'meters',
    135           type: 'distance', distanceValue: 1, distanceUnit: 'miles',
    136         },
    137         {
    138           splitValue: 800, splitUnit: 'meters',
    139           type: 'distance', distanceValue: 5, distanceUnit: 'kilometers',
    140         },
    141         {
    142           splitValue: 1600, splitUnit: 'meters',
    143           type: 'time', time: 3600,
    144         },
    145         {
    146           splitValue: 2, splitUnit: 'miles',
    147           type: 'time', time: 7200,
    148         },
    149       ],
    150     },
    151     'B': {
    152       name: 'Workout targets #2',
    153       targets: [
    154         {
    155           distanceUnit: 'miles', distanceValue: 2,
    156           splitUnit: 'meters', splitValue: 400,
    157           type: 'distance',
    158         },
    159         {
    160           time: 6000,
    161           splitUnit: 'kilometers', splitValue: 2,
    162           type: 'time',
    163         },
    164         {
    165           distanceUnit: 'kilometers', distanceValue: 5,
    166           splitUnit: 'miles', splitValue: 1,
    167           type: 'distance'
    168         },
    169       ],
    170     },
    171   };
    172 
    173   // Initialize component
    174   const wrapper = shallowMount(WorkoutCalculator);
    175 
    176   // Update options
    177   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    178     defaultUnitSystem: 'imperial',
    179     racePredictionOptions: {
    180       model: 'CameronModel',
    181       riegelExponent: 1.3,
    182     },
    183   }, 'globalOptions');
    184 
    185   // Assert data saved to localStorage
    186   expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({
    187     defaultUnitSystem: 'imperial',
    188     racePredictionOptions: {
    189       model: 'CameronModel',
    190       riegelExponent: 1.3,
    191     },
    192   }));
    193 
    194   // Update input race
    195   await wrapper.findComponent({ name: 'pace-input' }).setValue({
    196     distanceValue: 1,
    197     distanceUnit: 'miles',
    198     time: 600,
    199   });
    200 
    201   // Assert data saved to localStorage
    202   expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
    203     customTargetNames: false,
    204     input: {
    205       distanceValue: 1,
    206       distanceUnit: 'miles',
    207       time: 600,
    208     },
    209     selectedTargetSet: '_workout_targets',
    210   }));
    211 
    212   // Update target name customization
    213   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    214     customTargetNames: true,
    215     input: {
    216       distanceValue: 1,
    217       distanceUnit: 'miles',
    218       time: 600,
    219     },
    220     selectedTargetSet: '_workout_targets',
    221   }, 'options');
    222 
    223   // Assert data saved to localStorage
    224   expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
    225     customTargetNames: true,
    226     input: {
    227       distanceValue: 1,
    228       distanceUnit: 'miles',
    229       time: 600,
    230     },
    231     selectedTargetSet: '_workout_targets',
    232   }));
    233 
    234   // Update target sets and selected target set
    235   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue(targetSets,
    236     'targetSets');
    237   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    238     customTargetNames: true,
    239     input: {
    240       distanceValue: 1,
    241       distanceUnit: 'miles',
    242       time: 600,
    243     },
    244     selectedTargetSet: 'B',
    245   }, 'options');
    246 
    247   // Assert data saved to localStorage
    248   expect(localStorage.getItem('running-tools.workout-calculator-target-sets'))
    249     .to.equal(JSON.stringify(targetSets));
    250   expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
    251     customTargetNames: true,
    252     input: {
    253       distanceValue: 1,
    254       distanceUnit: 'miles',
    255       time: 600,
    256     },
    257     selectedTargetSet: 'B',
    258   }));
    259 });
    260 
    261 test('should correctly predict workout splits', async () => {
    262   // Initialize component
    263   const wrapper = shallowMount(WorkoutCalculator);
    264 
    265   // Enter input race data
    266   await wrapper.findComponent({ name: 'pace-input' }).setValue({
    267     distanceValue: 5,
    268     distanceUnit: 'kilometers',
    269     time: 1200,
    270   });
    271 
    272   // Calculate result
    273   const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
    274   const result = calculateResult({
    275     splitValue: 1, splitUnit: 'kilometers',
    276     type: 'distance', distanceValue: 10, distanceUnit: 'kilometers',
    277   });
    278 
    279   // Assert result is correct
    280   expect(result.key).to.equal('1 km @ 10 km');
    281   expect(result.value).to.equal('4:09.48');
    282   expect(result.result).to.equal('value');
    283   expect(result.sort).to.be.closeTo(249.48, 0.01);
    284 });
    285 
    286 test('should correctly handle null target set', async () => {
    287   // Initialize component
    288   const wrapper = shallowMount(WorkoutCalculator);
    289 
    290   // Switch to invalid target set
    291   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    292     customTargetNames: false,
    293     input: {
    294       distanceValue: 5,
    295       distanceUnit: 'kilometers',
    296       time: 1200,
    297     },
    298     selectedTargetSet: 'does_not_exist',
    299   }, 'options');
    300 
    301   // Assert empty array passed to SingleOutputTable component
    302   expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets).to.deep.equal([]);
    303 
    304   // Switch to valid target set
    305   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    306     customTargetNames: false,
    307     input: {
    308       distanceValue: 5,
    309       distanceUnit: 'kilometers',
    310       time: 1200,
    311     },
    312     selectedTargetSet: '_workout_targets',
    313   }, 'options');
    314 
    315   // Assert valid targets passed to SingleOutputTable component
    316   const workoutTargets = defaultTargetSets._workout_targets.targets;
    317   expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
    318     .to.deep.equal(workoutTargets);
    319 });
    320 
    321 test('should correctly calculate results according to options', async () => {
    322   // Initialize component
    323   const wrapper = shallowMount(WorkoutCalculator);
    324 
    325   // Enter input race data
    326   await wrapper.findComponent({ name: 'pace-input' }).setValue({
    327     distanceValue: 2,
    328     distanceUnit: 'miles',
    329     time: 630,
    330   });
    331 
    332   // Update model and Riegel exponent
    333   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    334     defaultUnitSystem: 'imperial',
    335     racePredictionOptions: {
    336       model: 'RiegelModel',
    337       riegelExponent: 1.10,
    338     },
    339   }, 'globalOptions');
    340 
    341   // Calculate result
    342   const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
    343   let result = calculateResult({
    344     customName: 'foo',
    345     splitValue: 1, splitUnit: 'kilometers',
    346     type: 'distance', distanceValue: 10, distanceUnit: 'kilometers',
    347   });
    348 
    349   // Assert result is correct
    350   expect(result.key).to.equal('1 km @ 10 km');
    351   expect(result.value).to.equal('3:39.23');
    352 
    353   // Update target name customization
    354   await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
    355     customTargetNames: true,
    356     input: {
    357       distanceValue: 2,
    358       distanceUnit: 'miles',
    359       time: 630,
    360     },
    361     selectedTargetSet: '_workout_targets',
    362   }, 'options');
    363 
    364   // Calculate result
    365   result = calculateResult({
    366     customName: 'foo',
    367     splitValue: 1, splitUnit: 'kilometers',
    368     type: 'distance', distanceValue: 10, distanceUnit: 'kilometers',
    369   });
    370 
    371   // Assert result is correct
    372   expect(result.key).to.equal('foo');
    373   expect(result.value).to.equal('3:39.23');
    374 });
    375 
    376 test('should correctly set AdvancedOptionsInput type prop', async () => {
    377   // Initialize component
    378   const wrapper = shallowMount(WorkoutCalculator);
    379 
    380   // Assert type prop is correctly set
    381   expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('workout');
    382 });