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 });