Skip to content

Commit

Permalink
Implement RaceOptions component
Browse files Browse the repository at this point in the history
  • Loading branch information
ashermorgan committed Jun 1, 2024
1 parent 1a03251 commit 21966fd
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 41 deletions.
30 changes: 30 additions & 0 deletions src/components/RaceOptions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
Prediction Model:
<select v-model="model.model" aria-label="Prediction model">
<option value="AverageModel">Average</option>
<option value="PurdyPointsModel">Purdy Points Model</option>
<option value="VO2MaxModel">V&#775;O&#8322; Max Model</option>
<option value="CameronModel">Cameron's Model</option>
<option value="RiegelModel">Riegel's Model</option>
</select>
</div>
<div>
Riegel Exponent:
<decimal-input v-model="model.riegelExponent" aria-label="Riegel exponent" :min="1" :max="1.3"
:digits="2" :step="0.01"/>
(default: 1.06)
</div>
</template>

<script setup>
import DecimalInput from '@/components/DecimalInput.vue';
const model = defineModel({
type: Object,
default: {
model: 'AverageModel',
riegelExponent: 1.06,
},
});
</script>
41 changes: 12 additions & 29 deletions src/views/RaceCalculator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,7 @@
<target-set-selector v-model:selectedTargetSet="selectedTargetSet"
v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
</div>
<div>
Prediction Model:
<select v-model="model" aria-label="Prediction model">
<option value="AverageModel">Average</option>
<option value="PurdyPointsModel">Purdy Points Model</option>
<option value="VO2MaxModel">V&#775;O&#8322; Max Model</option>
<option value="CameronModel">Cameron's Model</option>
<option value="RiegelModel">Riegel's Model</option>
</select>
</div>
<div>
Riegel Exponent:
<decimal-input v-model="riegelExponent" aria-label="Riegel exponent" :min="1" :max="1.3"
:digits="2" :step="0.01"/>
(default: 1.06)
</div>
<race-options v-model="options"/>
</details>

<h2>Equivalent Race Results</h2>
Expand All @@ -69,8 +54,8 @@ import raceUtils from '@/utils/races';
import targetUtils from '@/utils/targets';
import unitUtils from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
import PaceInput from '@/components/PaceInput.vue';
import RaceOptions from '@/components/RaceOptions.vue';
import SimpleTargetTable from '@/components/SimpleTargetTable.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
Expand All @@ -93,12 +78,10 @@ const defaultUnitSystem = useStorage('default-unit-system', unitUtils.detectDefa
/**
* The race prediction model
*/
const model = useStorage('race-calculator-model', 'AverageModel');
/**
* The value of the exponent in Riegel's Model
*/
const riegelExponent = useStorage('race-calculator-riegel-exponent', 1.06);
const options = useStorage('race-calculator-options', {
model: 'AverageModel',
riegelExponent: 1.06,
});
/**
* The current selected target set
Expand Down Expand Up @@ -131,11 +114,11 @@ function predictResult(target) {
// Get prediction
let time;
switch (model.value) {
switch (options.value.model) {
default:
case 'AverageModel':
time = raceUtils.AverageModel.predictTime(d1.value, input.value.time, d2,
riegelExponent.value);
options.value.riegelExponent);
break;
case 'PurdyPointsModel':
time = raceUtils.PurdyPointsModel.predictTime(d1.value, input.value.time, d2);
Expand All @@ -145,7 +128,7 @@ function predictResult(target) {
break;
case 'RiegelModel':
time = raceUtils.RiegelModel.predictTime(d1.value, input.value.time, d2,
riegelExponent.value);
options.value.riegelExponent);
break;
case 'CameronModel':
time = raceUtils.CameronModel.predictTime(d1.value, input.value.time, d2);
Expand All @@ -157,11 +140,11 @@ function predictResult(target) {
} else {
// Get prediction
let distance;
switch (model.value) {
switch (options.value.model) {
default:
case 'AverageModel':
distance = raceUtils.AverageModel.predictDistance(input.value.time, d1.value, target.time,
riegelExponent.value);
options.value.riegelExponent);
break;
case 'PurdyPointsModel':
distance = raceUtils.PurdyPointsModel.predictDistance(input.value.time, d1.value,
Expand All @@ -172,7 +155,7 @@ function predictResult(target) {
break;
case 'RiegelModel':
distance = raceUtils.RiegelModel.predictDistance(input.value.time, d1.value, target.time,
riegelExponent.value);
options.value.riegelExponent);
break;
case 'CameronModel':
distance = raceUtils.CameronModel.predictDistance(input.value.time, d1.value, target.time);
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/components/RaceOptions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import RaceOptions from '@/components/RaceOptions.vue';

test('should be initialized to modelValue', () => {
// Initialize component
const wrapper = shallowMount(RaceOptions, {
propsData: {
modelValue: {
model: 'PurdyPointsModel',
riegelExponent: 1.2,
}
},
});

// Assert input fields are correct
expect(wrapper.find('select').element.value).to.equal('PurdyPointsModel');
expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
});

test('should update modelValue when inputs are modified', async () => {
// Initialize component
const wrapper = shallowMount(RaceOptions);

// Update model
await wrapper.find('select').setValue('CameronModel');
expect(wrapper.vm.modelValue).to.deep.equal({
model: 'CameronModel',
riegelExponent: 1.06,
});

// Update Riegel exponent
await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
expect(wrapper.vm.modelValue).to.deep.equal({
model: 'CameronModel',
riegelExponent: 1.3,
});
});
36 changes: 24 additions & 12 deletions tests/unit/views/RaceCalculator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ test('should correctly calculate results according to advanced model options', a
});

// Switch model
await wrapper.find('select[aria-label="Prediction model"]').setValue('RiegelModel');
await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
model: 'RiegelModel',
riegelExponent: 1.06, // default value
});

// Calculate result
const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult;
Expand All @@ -146,8 +149,10 @@ test('should correctly calculate results according to advanced model options', a
expect(result.time).to.be.closeTo(2502, 1);

// Update Riegel Exponent
expect(wrapper.findComponent('[aria-label="Riegel exponent"').vm.modelValue).to.equal(1.06);
await wrapper.findComponent('[aria-label="Riegel exponent"').setValue(1);
await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
model: 'RiegelModel', // existing value
riegelExponent: 1,
});

// Calculate result
result = calculateResult({
Expand Down Expand Up @@ -240,28 +245,35 @@ test('should save default units setting to localStorage when modified', async ()

test('should load advanced model options from localStorage', async () => {
// Initialize localStorage
localStorage.setItem('running-tools.race-calculator-model', '"PurdyPointsModel"');
localStorage.setItem('running-tools.race-calculator-riegel-exponent', '1.20');
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
model: 'PurdyPointsModel',
riegelExponent: 1.2,
}));

// Initialize component
const wrapper = shallowMount(RaceCalculator);

// Assert data loaded
expect(wrapper.find('select[aria-label="Prediction model"]').element.value)
.to.equal('PurdyPointsModel');
expect(wrapper.findComponent('[aria-label="Riegel exponent"]').vm.modelValue).to.equal(1.20);
expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({
model: 'PurdyPointsModel',
riegelExponent: 1.2,
});
});

test('should save advanced model options to localStorage when modified', async () => {
// Initialize component
const wrapper = shallowMount(RaceCalculator);

// Update advanced model options
await wrapper.find('select[aria-label="Prediction model"]').setValue('CameronModel');
await wrapper.findComponent('[aria-label="Riegel exponent"]').setValue(1.30);
await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
model: 'CameronModel',
riegelExponent: 1.30,
});

// Assert data saved to localStorage
expect(localStorage.getItem('running-tools.race-calculator-model')).to.equal('"CameronModel"');
expect(localStorage.getItem('running-tools.race-calculator-riegel-exponent')).to.equal('1.3');
expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({
model: 'CameronModel',
riegelExponent: 1.3,
}));
});

0 comments on commit 21966fd

Please sign in to comment.