running-tools

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

RaceCalculator.spec.js (11953B)


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