running-tools

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

commit 21966fd2e29e494d9ec2eb8074e441faf5c88168
parent 1a032514433fbeab6092e6117636a02b23f7bb8c
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat,  1 Jun 2024 10:27:27 -0700

Implement RaceOptions component

Diffstat:
Asrc/components/RaceOptions.vue | 30++++++++++++++++++++++++++++++
Msrc/views/RaceCalculator.vue | 41++++++++++++-----------------------------
Atests/unit/components/RaceOptions.spec.js | 38++++++++++++++++++++++++++++++++++++++
Mtests/unit/views/RaceCalculator.spec.js | 36++++++++++++++++++++++++------------
4 files changed, 104 insertions(+), 41 deletions(-)

diff --git a/src/components/RaceOptions.vue b/src/components/RaceOptions.vue @@ -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> diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -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> @@ -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'; @@ -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 @@ -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); @@ -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); @@ -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, @@ -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); diff --git a/tests/unit/components/RaceOptions.spec.js b/tests/unit/components/RaceOptions.spec.js @@ -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, + }); +}); diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -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; @@ -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({ @@ -240,16 +245,19 @@ 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 () => { @@ -257,11 +265,15 @@ test('should save advanced model options to localStorage when modified', async ( 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, + })); });