running-tools

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

commit cd745cb3c2383acfad03df9d1d576bb3278540b9
parent 6b28796f0472785d82c9effcc626d6232d4d54c1
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 17 Aug 2025 18:47:37 -0700

Sync race prediction options globally

Move defaultUnitSystem and *-calculator-options.racePredictionOptions to
a new global-options localStorage entry. Migration scripts and
corresponding end-to-end test not yet updated.

Diffstat:
Msrc/components/AdvancedOptionsInput.vue | 35++++++++++++++++++++---------------
Msrc/core/calculators.ts | 21+++++++++++----------
Msrc/core/racePrediction.ts | 8++++++++
Msrc/views/BatchCalculator.vue | 19++++++++++---------
Msrc/views/PaceCalculator.vue | 13++++++-------
Msrc/views/RaceCalculator.vue | 18+++++++++---------
Msrc/views/SplitCalculator.vue | 16++++++++--------
Msrc/views/WorkoutCalculator.vue | 13++++++-------
Mtests/e2e/batch-calculator.spec.js | 16++++++++--------
Mtests/e2e/cross-calculator.spec.js | 56+++++++++++++++++++++++++++++---------------------------
Mtests/unit/components/AdvancedOptionsInput.spec.js | 148++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mtests/unit/views/BatchCalculator.spec.js | 310++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mtests/unit/views/PaceCalculator.spec.js | 276++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mtests/unit/views/RaceCalculator.spec.js | 382+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mtests/unit/views/SplitCalculator.spec.js | 234+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtests/unit/views/WorkoutCalculator.spec.js | 368+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
16 files changed, 1096 insertions(+), 837 deletions(-)

diff --git a/src/components/AdvancedOptionsInput.vue b/src/components/AdvancedOptionsInput.vue @@ -1,7 +1,7 @@ <template> <div> Default units: - <select v-model="defaultUnitSystem" aria-label="Default units"> + <select v-model="globalOptions.defaultUnitSystem" aria-label="Default units"> <option value="imperial">Miles</option> <option value="metric">Kilometers</option> </select> @@ -9,7 +9,8 @@ <div> Target set: - <target-set-selector :setType="props.type" :default-unit-system="defaultUnitSystem" + <target-set-selector :setType="props.type" + :default-unit-system="globalOptions.defaultUnitSystem" v-model:selected-target-set="options.selectedTargetSet" v-model:target-sets="targetSets" :customWorkoutNames="props.type === Calculators.Workout ? (options as WorkoutOptions).customTargetNames : false"/> @@ -33,7 +34,7 @@ <div v-if="props.type === Calculators.Race || props.type === Calculators.Workout"> Prediction model: - <select v-model="(options as RaceOptions).predictionOptions.model" + <select v-model="globalOptions.racePredictionOptions.model" aria-label="Prediction model"> <option :value="RacePredictionModels.AverageModel">Average</option> <option :value="RacePredictionModels.PurdyPointsModel">Purdy Points Model</option> @@ -44,10 +45,10 @@ </div> <div v-if="props.type === Calculators.Race || props.type === Calculators.Workout" - v-show="(options as RaceOptions).predictionOptions.model == RacePredictionModels.AverageModel - || (options as RaceOptions).predictionOptions.model == RacePredictionModels.RiegelModel"> + v-show="globalOptions.racePredictionOptions.model == RacePredictionModels.AverageModel + || globalOptions.racePredictionOptions.model == RacePredictionModels.RiegelModel"> Riegel exponent: - <decimal-input v-model="(options as RaceOptions).predictionOptions.riegelExponent" + <decimal-input v-model="globalOptions.racePredictionOptions.riegelExponent" aria-label="Riegel exponent" :min="1" :max="1.3" :digits="2" :step="0.01"/> (default: 1.06) </div> @@ -55,11 +56,11 @@ <script setup lang="ts"> import { Calculators } from '@/core/calculators'; -import type { BatchOptions, StandardOptions, RaceOptions, +import type { BatchOptions, GlobalOptions, StandardOptions, RaceOptions, WorkoutOptions } from '@/core/calculators'; import { RacePredictionModels } from '@/core/racePrediction'; import type { TargetSets } from '@/core/targets'; -import { UnitSystems, formatDistance } from '@/core/units'; +import { formatDistance } from '@/core/units'; import type { DistanceTime } from '@/core/units'; import DecimalInput from '@/components/DecimalInput.vue'; @@ -69,11 +70,6 @@ import useObjectModel from '@/composables/useObjectModel'; type CalculatorOptions = StandardOptions | RaceOptions | WorkoutOptions; -/* - * The default unit system - */ -const defaultUnitSystem = defineModel<UnitSystems>('defaultUnitSystem'); - const props = defineProps<{ /* * The batch calculator input (if applicable, used to generate custom batch label placeholder) @@ -86,6 +82,11 @@ const props = defineProps<{ batchOptions?: BatchOptions, /* + * The global options + */ + globalOptions: GlobalOptions, + + /* * The calculator options */ options: CalculatorOptions, @@ -101,10 +102,14 @@ const props = defineProps<{ targetSets: TargetSets, }>(); -// Generate internal refs tied to options and targetSets props -const emit = defineEmits(['update:batchOptions', 'update:options', 'update:targetSets']); +// Generate internal refs tied to batchOptions, globalOptions, options and targetSets props +const emit = defineEmits([ + 'update:batchOptions', 'update:globalOptions', 'update:options', 'update:targetSets' +]); const batchOptions = useObjectModel<BatchOptions | undefined>(() => props.batchOptions, (x) => emit('update:batchOptions', x)); +const globalOptions = useObjectModel<GlobalOptions>(() => props.globalOptions, (x) => + emit('update:globalOptions', x)); const options = useObjectModel<CalculatorOptions>(() => props.options, (x) => emit('update:options', x)); const targetSets = useObjectModel<TargetSets>(() => props.targetSets, (x) => diff --git a/src/core/calculators.ts b/src/core/calculators.ts @@ -6,8 +6,8 @@ import * as racePrediction from '@/core/racePrediction'; import type { RacePredictionOptions } from '@/core/racePrediction'; import { TargetTypes, workoutTargetToString } from '@/core/targets'; import type { StandardTarget, WorkoutTarget } from '@/core/targets'; -import { DistanceUnits, UnitSystems, convertDistance, formatDistance, formatDuration, formatPace, - getDefaultDistanceUnit, getDefaultPaceUnit } from '@/core/units'; +import { DistanceUnits, UnitSystems, convertDistance, detectDefaultUnitSystem, formatDistance, + formatDuration, formatPace, getDefaultDistanceUnit, getDefaultPaceUnit } from '@/core/units'; import type { DistanceTime } from '@/core/units'; /* @@ -35,12 +35,14 @@ export interface RaceStats { /* * The type for the options specific to each calculator */ +export interface GlobalOptions { + defaultUnitSystem: UnitSystems, + racePredictionOptions: RacePredictionOptions, +}; export interface StandardOptions { selectedTargetSet: string, -} -export interface RaceOptions extends StandardOptions { - predictionOptions: RacePredictionOptions, }; +export type RaceOptions = StandardOptions; export interface WorkoutOptions extends RaceOptions { customTargetNames: boolean, }; @@ -73,6 +75,10 @@ export interface TargetResult { /* * The default input and options for each calculator */ +export const defaultGlobalOptions: GlobalOptions = { + defaultUnitSystem: detectDefaultUnitSystem(), + racePredictionOptions: racePrediction.defaultRacePredictionOptions, +}; export const defaultInput: DistanceTime = { distanceValue: 5, distanceUnit: DistanceUnits.Kilometers, @@ -88,10 +94,6 @@ export const defaultPaceOptions: StandardOptions = { selectedTargetSet: '_pace_targets', }; export const defaultRaceOptions: RaceOptions = { - predictionOptions: { - model: racePrediction.RacePredictionModels.AverageModel, - riegelExponent: 1.06, - }, selectedTargetSet: '_race_targets', }; export const defaultSplitOptions: StandardOptions = { @@ -99,7 +101,6 @@ export const defaultSplitOptions: StandardOptions = { }; export const defaultWorkoutOptions: WorkoutOptions = { customTargetNames: false, - ...defaultRaceOptions, selectedTargetSet: '_workout_targets', }; diff --git a/src/core/racePrediction.ts b/src/core/racePrediction.ts @@ -22,6 +22,14 @@ export interface RacePredictionOptions { }; /* + * The default race prediction options + */ +export const defaultRacePredictionOptions = { + model: RacePredictionModels.AverageModel, + riegelExponent: 1.06, +}; + +/* * The type for internal variables used by the Purdy Points race prediction model */ interface PurdyPointsVariables { diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue @@ -28,7 +28,7 @@ <h2>Advanced Options</h2> </summary> <advanced-options-input :batch-input="input" v-model:batch-options="options" - v-model:defaultUnitSystem="defaultUnitSystem" v-model:options="calcOptions" + v-model:globalOptions="globalOptions" v-model:options="calcOptions" v-model:targetSets="targetSets" :type="options.calculator"/> </details> @@ -44,10 +44,10 @@ import { computed } from 'vue'; import * as calculators from '@/core/calculators'; -import type { BatchOptions, RaceOptions, StandardOptions, TargetResult, +import type { BatchOptions, GlobalOptions, RaceOptions, StandardOptions, TargetResult, WorkoutOptions } from '@/core/calculators'; import * as targetUtils from '@/core/targets'; -import { UnitSystems, detectDefaultUnitSystem, formatDistance } from '@/core/units'; +import { formatDistance } from '@/core/units'; import type { Distance, DistanceTime } from '@/core/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; @@ -70,9 +70,9 @@ const options = useStorage<BatchOptions>('batch-calculator-options', calculators.defaultBatchOptions); /* - * The default unit system + * The global options */ -const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); +const globalOptions = useStorage<GlobalOptions>('global-options', calculators.defaultGlobalOptions); /* * The target sets for each calculator @@ -193,16 +193,17 @@ const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({ const calculateResult = computed<(x: DistanceTime, y: targetUtils.Target) => TargetResult>(() => { switch(options.value.calculator) { case (calculators.Calculators.Pace): { - return (x,y) => calculators.calculatePaceResults(x, y, defaultUnitSystem.value, false); + return (x,y) => calculators.calculatePaceResults(x, y, globalOptions.value.defaultUnitSystem, + false); } case (calculators.Calculators.Race): { - return (x,y) => calculators.calculateRaceResults(x, y, raceOptions.value.predictionOptions, - defaultUnitSystem.value, false); + return (x,y) => calculators.calculateRaceResults(x, y, + globalOptions.value.racePredictionOptions, globalOptions.value.defaultUnitSystem, false); } default: case (calculators.Calculators.Workout): { return (x,y) => calculators.calculateWorkoutResults(x, y as targetUtils.WorkoutTarget, - workoutOptions.value.predictionOptions, workoutOptions.value.customTargetNames, false); + globalOptions.value.racePredictionOptions, workoutOptions.value.customTargetNames, false); } } }); diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -9,25 +9,24 @@ <summary> <h2>Advanced Options</h2> </summary> - <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem" + <advanced-options-input v-model:globalOptions="globalOptions" v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Pace"/> </details> <h2>Equivalent Paces</h2> <single-output-table class="output" :calculate-result="x => - calculatePaceResults(input, x, defaultUnitSystem, true)" + calculatePaceResults(input, x, globalOptions.defaultUnitSystem, true)" :targets="targetSets[options.selectedTargetSet] ? targetSets[options.selectedTargetSet].targets : []"/> </div> </template> <script setup lang="ts"> -import { Calculators, calculatePaceResults, defaultInput, +import { Calculators, calculatePaceResults, defaultGlobalOptions, defaultInput, defaultPaceOptions } from '@/core/calculators'; -import type { StandardOptions } from '@/core/calculators'; +import type { GlobalOptions, StandardOptions } from '@/core/calculators'; import { defaultPaceTargetSets } from '@/core/targets'; import type { StandardTargetSets } from '@/core/targets'; -import { UnitSystems, detectDefaultUnitSystem } from '@/core/units'; import type { DistanceTime } from '@/core/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; @@ -42,9 +41,9 @@ import useStorage from '@/composables/useStorage'; const input = useStorage<DistanceTime>('pace-calculator-input', defaultInput); /* - * The default unit system + * The global options */ -const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); +const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions); /* * The current selected target set diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -26,14 +26,14 @@ <summary> <h2>Advanced Options</h2> </summary> - <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem" + <advanced-options-input v-model:globalOptions="globalOptions" v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Race"/> </details> <h2>Equivalent Race Results</h2> <single-output-table class="output" show-pace - :calculate-result="x => calculateRaceResults(input, x, options.predictionOptions, - defaultUnitSystem, true)" + :calculate-result="x => calculateRaceResults(input, x, globalOptions.racePredictionOptions, + globalOptions.defaultUnitSystem, true)" :targets="targetSets[options.selectedTargetSet] ? targetSets[options.selectedTargetSet].targets : []"/> </div> @@ -42,12 +42,12 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { Calculators, calculateRaceResults, calculateRaceStats, defaultInput, +import { Calculators, calculateRaceResults, calculateRaceStats, defaultGlobalOptions, defaultInput, defaultRaceOptions } from '@/core/calculators'; -import type { RaceOptions, RaceStats } from '@/core/calculators'; +import type { GlobalOptions, RaceOptions, RaceStats } from '@/core/calculators'; import { defaultRaceTargetSets } from '@/core/targets'; import type { StandardTargetSets } from '@/core/targets'; -import { UnitSystems, detectDefaultUnitSystem, formatNumber } from '@/core/units'; +import { formatNumber } from '@/core/units'; import type { DistanceTime } from '@/core/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; @@ -62,9 +62,9 @@ import useStorage from '@/composables/useStorage'; const input = useStorage<DistanceTime>('race-calculator-input', defaultInput); /* - * The default unit system + * The global options */ -const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); +const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions); /* * The race calculator options @@ -72,7 +72,7 @@ const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectD const options = useStorage<RaceOptions>('race-calculator-options', defaultRaceOptions); /* - * The target sets + * The race calculator target sets */ const targetSets = useStorage<StandardTargetSets>('race-calculator-target-sets', defaultRaceTargetSets); diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -1,12 +1,13 @@ <template> <div class="calculator"> <div class="input"> - <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem" + <advanced-options-input v-model:globalOptions="globalOptions" v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Split"/> </div> <div class="output"> - <split-output-table :default-unit-system="defaultUnitSystem" v-model="targetSet"/> + <split-output-table :default-unit-system="globalOptions.defaultUnitSystem" + v-model="targetSet"/> </div> </div> </template> @@ -14,11 +15,10 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { Calculators, defaultSplitOptions } from '@/core/calculators'; -import type { StandardOptions } from '@/core/calculators'; +import { Calculators, defaultGlobalOptions, defaultSplitOptions } from '@/core/calculators'; +import type { GlobalOptions, StandardOptions } from '@/core/calculators'; import { defaultSplitTargetSets } from '@/core/targets'; import type { SplitTargetSets } from '@/core/targets'; -import { UnitSystems, detectDefaultUnitSystem } from '@/core/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; import SplitOutputTable from '@/components/SplitOutputTable.vue'; @@ -26,9 +26,9 @@ import SplitOutputTable from '@/components/SplitOutputTable.vue'; import useStorage from '@/composables/useStorage'; /* - * The default unit system + * The global options */ -const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); +const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions); /* * The split calculator options @@ -36,7 +36,7 @@ const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectD const options = useStorage<StandardOptions>('split-calculator-options', defaultSplitOptions); /* - * The default output targets + * The split calculator target sets */ const targetSets = useStorage<SplitTargetSets>('split-calculator-target-sets', defaultSplitTargetSets); diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue @@ -9,26 +9,25 @@ <summary> <h2>Advanced Options</h2> </summary> - <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem" + <advanced-options-input v-model:globalOptions="globalOptions" v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Workout"/> </details> <h2>Workout Splits</h2> <single-output-table class="output" :calculate-result="x => calculateWorkoutResults(input, x as WorkoutTarget, - options.predictionOptions, options.customTargetNames, true)" + globalOptions.racePredictionOptions, options.customTargetNames, true)" :targets="targetSets[options.selectedTargetSet] ? targetSets[options.selectedTargetSet].targets : []"/> </div> </template> <script setup lang="ts"> -import { Calculators, calculateWorkoutResults, defaultInput, +import { Calculators, calculateWorkoutResults, defaultGlobalOptions, defaultInput, defaultWorkoutOptions } from '@/core/calculators'; -import type { WorkoutOptions } from '@/core/calculators'; +import type { GlobalOptions, WorkoutOptions } from '@/core/calculators'; import { defaultWorkoutTargetSets } from '@/core/targets'; import type { WorkoutTarget, WorkoutTargetSets } from '@/core/targets'; -import { UnitSystems, detectDefaultUnitSystem } from '@/core/units'; import type { DistanceTime } from '@/core/units'; import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue'; @@ -43,9 +42,9 @@ import useStorage from '@/composables/useStorage'; const input = useStorage<DistanceTime>('workout-calculator-input', defaultInput); /* - * The default unit system + * The global options */ -const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); +const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions); /* * The race prediction options diff --git a/tests/e2e/batch-calculator.spec.js b/tests/e2e/batch-calculator.spec.js @@ -105,15 +105,15 @@ test('Batch calculator', async ({ page }) => { await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:15'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:44'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); // Change Riegel exponent - await expect(page.getByLabel('Prediction model')).toHaveValue('AverageModel'); + await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel'); await page.getByLabel('Riegel Exponent').fill('1.12'); // Assert race results are correct @@ -124,7 +124,7 @@ test('Batch calculator', async ({ page }) => { await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:40'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); } @@ -141,7 +141,7 @@ test('Batch calculator', async ({ page }) => { await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:40'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); } @@ -164,7 +164,7 @@ test('Batch calculator', async ({ page }) => { await expect(page.getByRole('row')).toHaveCount(16); } - // Assert workout results are correct (inputs and options not reset) + // Assert workout results are correct (inputs not reset, but updated options are used) { await page.getByLabel('Calculator').selectOption('Workout Calculator'); await expect(page.getByLabel('Target name customization')).toHaveValue("true"); @@ -172,10 +172,10 @@ test('Batch calculator', async ({ page }) => { await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:41'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:45'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:17'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:22'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row')).toHaveCount(16); } diff --git a/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js @@ -110,9 +110,9 @@ test('Cross-calculator', async ({ page }) => { await page.getByLabel('Input race duration minutes').fill('5'); await page.getByLabel('Input race duration seconds').fill('1'); - // Change prediction model and enable target name customization + // Change riegel exponent and enable target name customization await page.getByText('Advanced Options').click(); - await page.getByLabel('Prediction model').selectOption('V̇O₂ Max Model'); + await page.getByLabel('Riegel Exponent').fill('1.12'); await page.getByLabel('Target name customization').selectOption('Enabled'); // Change default units (should update on other calculators too) @@ -125,15 +125,15 @@ test('Cross-calculator', async ({ page }) => { await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Batch Calculator' }).click(); - // Assert pace results are correct (inputs and options not reset) + // Assert race results are correct (inputs and options not reset) await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); @@ -157,10 +157,10 @@ test('Cross-calculator', async ({ page }) => { await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:42'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:45'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:17'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:22'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row')).toHaveCount(16); @@ -182,8 +182,8 @@ test('Cross-calculator', async ({ page }) => { await page.getByRole('button', { name: 'Race Calculator' }).click(); // Assert race predictions are correct (input race not resset and new prediction model loaded) - await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km'); - await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.87' + '3:21 / km'); + await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:49.86' + '3:00 / km'); + await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '17:11.78' + '3:26 / km'); await expect(page.getByRole('row')).toHaveCount(17); // Return to split calculator @@ -212,16 +212,22 @@ test('Cross-calculator', async ({ page }) => { // Assert workout splits are correct (input race and prediction model not reset) await expect(page.getByRole('row').nth(1)).toHaveText('400 m @ 1 mi' + '1:14.81'); - await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '5:53.56'); + await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '6:30.40'); await expect(page.getByRole('row')).toHaveCount(5); } // Assert localStorage entries are correct { - // Assert general localStorage entries are correct + // Assert global localStorage entries are correct expect(await page.evaluate(() => localStorage.length)).toEqual(16); - expect(await page.evaluate(() => localStorage.getItem('running-tools.default-unit-system'))) - .toEqual(JSON.stringify('metric')); + expect(await page.evaluate(() => localStorage.getItem('running-tools.global-options'))) + .toEqual(JSON.stringify({ + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'RiegelModel', + riegelExponent: 1.12, + }, + })); // Assert localStorage entries for the batch calculator are correct expect(await page.evaluate(() => @@ -304,8 +310,6 @@ test('Cross-calculator', async ({ page }) => { })); expect(await page.evaluate(() => localStorage.getItem('running-tools.race-calculator-options'))).toEqual(JSON.stringify({ - model: 'RiegelModel', - riegelExponent: 1.06, selectedTargetSet: '_race_targets', })); expect(await page.evaluate(() => @@ -383,8 +387,6 @@ test('Cross-calculator', async ({ page }) => { expect(await page.evaluate(() => localStorage.getItem('running-tools.workout-calculator-options'))).toEqual(JSON.stringify({ customTargetNames: true, - model: 'VO2MaxModel', - riegelExponent: 1.06, selectedTargetSet: '_workout_targets', })); expect(await page.evaluate(() => @@ -416,19 +418,19 @@ test('Cross-calculator', async ({ page }) => { // Reload app and assert the updated options are loaded // Identical to the previous "go back and assert the options are not resset" section { - // Reload app and go to batch calculator - await page.goto('/'); + // Return to batch calculator + await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Batch Calculator' }).click(); - // Assert pace results are correct (inputs and options not reset) + // Assert race results are correct (inputs and options not reset) await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:12'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:42'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); @@ -452,10 +454,10 @@ test('Cross-calculator', async ({ page }) => { await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:42'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:45'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:17'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:22'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row')).toHaveCount(16); @@ -477,8 +479,8 @@ test('Cross-calculator', async ({ page }) => { await page.getByRole('button', { name: 'Race Calculator' }).click(); // Assert race predictions are correct (input race not resset and new prediction model loaded) - await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km'); - await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.87' + '3:21 / km'); + await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:49.86' + '3:00 / km'); + await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '17:11.78' + '3:26 / km'); await expect(page.getByRole('row')).toHaveCount(17); // Return to split calculator @@ -507,7 +509,7 @@ test('Cross-calculator', async ({ page }) => { // Assert workout splits are correct (input race and prediction model not reset) await expect(page.getByRole('row').nth(1)).toHaveText('400 m @ 1 mi' + '1:14.81'); - await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '5:53.56'); + await expect(page.getByRole('row').nth(3)).toHaveText('1600 m @ 1:00:00' + '6:30.40'); await expect(page.getByRole('row')).toHaveCount(5); } }); diff --git a/tests/unit/components/AdvancedOptionsInput.spec.js b/tests/unit/components/AdvancedOptionsInput.spec.js @@ -6,7 +6,13 @@ test('should be correctly render pace options according to props', () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { selectedTargetSet: 'B', }, @@ -66,12 +72,14 @@ test('should be correctly render race options according to props', () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', - options: { - predictionOptions: { + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { model: 'PurdyPointsModel', riegelExponent: 1.2, }, + }, + options: { selectedTargetSet: '_new', }, type: 'race', @@ -98,12 +106,14 @@ test('should render riegel exponent field only for supported race prediction mod // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', - options: { - predictionOptions: { + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { model: 'AverageModel', riegelExponent: 1.2, }, + }, + options: { selectedTargetSet: '_new', }, type: 'race', @@ -136,7 +146,13 @@ test('should be correctly render split options according to props', () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { selectedTargetSet: '_new', }, @@ -162,13 +178,15 @@ test('should be correctly render workout options according to props', () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', - options: { - customTargetNames: true, - predictionOptions: { + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { model: 'PurdyPointsModel', riegelExponent: 1.2, }, + }, + options: { + customTargetNames: true, selectedTargetSet: '_new', }, targetSets: {}, @@ -193,7 +211,13 @@ test('should only show batch column label field when applicable', async () => { // Initialize component with workout target name customization enabled const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { customTargetNames: true, predictionOptions: { @@ -225,7 +249,13 @@ test('should only show batch column label field when applicable', async () => { label: 'foo', rows: 15, }, - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { customTargetNames: false, // disabled predictionOptions: { @@ -254,7 +284,13 @@ test('should only show batch column label field when applicable', async () => { label: 'foo', rows: 15, }, - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { customTargetNames: true, // enabled predictionOptions: { @@ -285,7 +321,13 @@ test('should only show batch column label field when applicable', async () => { label: 'foo', rows: 15, }, - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { predictionOptions: { model: 'PurdyPointsModel', @@ -306,7 +348,13 @@ test('should pass correct props to TargetSetSelector', async () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, options: { customTargetNames: false, predictionOptions: { @@ -372,13 +420,15 @@ test('should emit input events when options are modified', async () => { // Initialize component const wrapper = shallowMount(AdvancedOptionsInput, { propsData: { - defaultUnitSystem: 'metric', - options: { - customTargetNames: false, - predictionOptions: { + globalOptions: { + defaultUnitSystem: 'metric', + racePredictionOptions: { model: 'AverageModel', riegelExponent: 1.06, }, + }, + options: { + customTargetNames: false, selectedTargetSet: '_new', }, targetSets: {}, @@ -412,7 +462,29 @@ test('should emit input events when options are modified', async () => { await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3); // Assert correct update events emitted - expect(wrapper.emitted()['update:defaultUnitSystem']).to.deep.equal([['imperial']]); + expect(wrapper.emitted()['update:globalOptions']).to.deep.equal([ + [{ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }], + [{ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.06, + }, + }], + [{ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.3, + }, + }], + ]); expect(wrapper.emitted()['update:targetSets']).to.deep.equal([[{ 'A': { name: '1st target set v2', @@ -433,36 +505,12 @@ test('should emit input events when options are modified', async () => { }]]); expect(wrapper.emitted()['update:options']).to.deep.equal([ [{ - customTargetNames: false, - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, - selectedTargetSet: 'B', + customTargetNames: false, + selectedTargetSet: 'B', }], [{ - customTargetNames: true, - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, - selectedTargetSet: 'B', - }], - [{ - customTargetNames: true, - predictionOptions: { - model: 'CameronModel', - riegelExponent: 1.06, - }, - selectedTargetSet: 'B', - }], - [{ - customTargetNames: true, - predictionOptions: { - model: 'CameronModel', - riegelExponent: 1.3, - }, - selectedTargetSet: 'B', + customTargetNames: true, + selectedTargetSet: 'B', }], ]); }); diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js @@ -4,7 +4,30 @@ import BatchCalculator from '@/views/BatchCalculator.vue'; beforeEach(() => { localStorage.clear(); -}) +}); + +test('should load global options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + })); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + }); +}); test('should load input from localStorage', async () => { // Initialize localStorage @@ -30,25 +53,6 @@ test('should load input from localStorage', async () => { }); }); -test('should save input to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(BatchCalculator); - - // Update input pace - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 2, - distanceUnit: 'miles', - time: 600, - }); - - // Assert input saved - expect(localStorage.getItem('running-tools.batch-calculator-input')).to.equal(JSON.stringify({ - distanceValue: 2, - distanceUnit: 'miles', - time: 600, - })); -}); - test('should load batch options from localStorage', async () => { // Initialize localStorage localStorage.setItem('running-tools.batch-calculator-options', JSON.stringify({ @@ -73,87 +77,6 @@ test('should load batch options from localStorage', async () => { }); }); -test('should save batch options to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(BatchCalculator); - - // Update increment value - await wrapper.findComponent({ name: 'time-input' }).setValue(32); - - // Assert options saved - expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ - calculator: 'workout', - increment: 32, - label: '', - rows: 20, - })); - - // Update number of rows - await wrapper.findComponent({ name: 'integer-input' }).setValue(15); - - // Assert options saved - expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ - calculator: 'workout', - increment: 32, - label: '', - rows: 15, - })); - - // Update batch column label - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - calculator: 'workout', - increment: 32, - label: 'foo', - rows: 15, - }, 'batch-options'); - - // Assert options saved - expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ - calculator: 'workout', - increment: 32, - label: 'foo', - rows: 15, - })); - - // Update active calculator - await wrapper.find('select[aria-label="Calculator"]').setValue('race'); - - // Assert options saved - expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ - calculator: 'race', - increment: 32, - label: 'foo', - rows: 15, - })); -}); - -test('should load default units setting from localStorage', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.default-unit-system', '"metric"'); - - // Initialize component - const wrapper = shallowMount(BatchCalculator); - - // Assert default units setting loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.defaultUnitSystem) - .to.equal('metric'); -}); - -test('should save default units setting from localStorage when modified', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.default-unit-system', '"metric"'); - - // Initialize component - const wrapper = shallowMount(BatchCalculator); - - // Change default units setting - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); -}); - test('should load calculator options from localStorage', async () => { // Initialize localStorage const selectedTargetSets = [ @@ -210,18 +133,10 @@ test('should load calculator options from localStorage', async () => { selectedTargetSet: 'A', })); localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, selectedTargetSet: 'C', })); localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ customTargetNames: true, - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.1, - }, selectedTargetSet: 'E', })); @@ -239,10 +154,6 @@ test('should load calculator options from localStorage', async () => { // Assert race calculator options are loaded await wrapper.find('select[aria-label="Calculator"]').setValue('race'); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, selectedTargetSet: 'C', }); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) @@ -252,16 +163,111 @@ test('should load calculator options from localStorage', async () => { await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ customTargetNames: true, - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.1, - }, selectedTargetSet: 'E', }); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[2].targets); }); +test('should save global options to localStorage when modified', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.default-unit-system', '"metric"'); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Change default units setting + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + }, 'globalOptions'); + + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.30, + }, + })); +}); + +test('should save input to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Update input pace + await wrapper.findComponent({ name: 'pace-input' }).setValue({ + distanceValue: 2, + distanceUnit: 'miles', + time: 600, + }); + + // Assert input saved + expect(localStorage.getItem('running-tools.batch-calculator-input')).to.equal(JSON.stringify({ + distanceValue: 2, + distanceUnit: 'miles', + time: 600, + })); +}); + +test('should save batch options to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Update increment value + await wrapper.findComponent({ name: 'time-input' }).setValue(32); + + // Assert options saved + expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ + calculator: 'workout', + increment: 32, + label: '', + rows: 20, + })); + + // Update number of rows + await wrapper.findComponent({ name: 'integer-input' }).setValue(15); + + // Assert options saved + expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ + calculator: 'workout', + increment: 32, + label: '', + rows: 15, + })); + + // Update batch column label + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + calculator: 'workout', + increment: 32, + label: 'foo', + rows: 15, + }, 'batch-options'); + + // Assert options saved + expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ + calculator: 'workout', + increment: 32, + label: 'foo', + rows: 15, + })); + + // Update active calculator + await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + + // Assert options saved + expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({ + calculator: 'race', + increment: 32, + label: 'foo', + rows: 15, + })); +}); + test('should save calculator options to localStorage when modified', async () => { // Initialize localStorage const selectedTargetSets = [ @@ -309,10 +315,6 @@ test('should save calculator options to localStorage when modified', async () => } })); localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, selectedTargetSet: 'D', })); localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({ @@ -326,10 +328,6 @@ test('should save calculator options to localStorage when modified', async () => })); localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ customWorkoutNames: false, - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, selectedTargetSet: 'F', })); @@ -347,10 +345,6 @@ test('should save calculator options to localStorage when modified', async () => // Update race calculator options await wrapper.find('select[aria-label="Calculator"]').setValue('race'); await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, selectedTargetSet: 'C', }, 'options'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) @@ -360,10 +354,6 @@ test('should save calculator options to localStorage when modified', async () => await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ customTargetNames: true, - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.1, - }, selectedTargetSet: 'E', }, 'options'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) @@ -374,18 +364,10 @@ test('should save calculator options to localStorage when modified', async () => selectedTargetSet: 'A', })); expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, selectedTargetSet: 'C', })); expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({ customTargetNames: true, - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.1, - }, selectedTargetSet: 'E', })); }); @@ -471,10 +453,6 @@ test('should pass correct input props to DoubleOutputTable', async () => { // Enable target name customization await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ customTargetNames: true, - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, selectedTargetSet: '_workout_targets', }, 'options'); @@ -531,6 +509,13 @@ test('should correctly set AdvancedOptionsInput props', async () => { // Assert props are correct for pace calculator await wrapper.find('select[aria-label="Calculator"]').setValue('pace'); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('pace'); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ selectedTargetSet: '_pace_targets', }); @@ -538,44 +523,49 @@ test('should correctly set AdvancedOptionsInput props', async () => { // Assert props are correct for race calculator await wrapper.find('select[aria-label="Calculator"]').setValue('race'); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('race'); - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - predictionOptions: { + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { model: 'AverageModel', riegelExponent: 1.06, }, + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ selectedTargetSet: '_race_targets', }); // Assert props are correct for workout calculator await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('workout'); - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - customTargetNames: false, - predictionOptions: { + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { model: 'AverageModel', riegelExponent: 1.06, }, + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ + customTargetNames: false, selectedTargetSet: '_workout_targets', }); }); test('should correctly calculate outputs', async () => { // Initialize localStorage - localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ - selectedTargetSet: '_race_targets', - predictionOptions: { + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { model: 'PurdyPointsModel', riegelExponent: 1.2, }, })); + localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + selectedTargetSet: '_race_targets', + })); localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + customTargetNames: false, selectedTargetSet: '_workout_targets', - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.1, - }, })); - localStorage.setItem('running-tools.default-unit-system', '"imperial"'); // Initialize component const wrapper = shallowMount(BatchCalculator); @@ -609,8 +599,8 @@ test('should correctly calculate outputs', async () => { const workoutTarget = { type: 'time', time: 3600, splitValue: 1, splitUnit: 'miles' }; const result = calculate(input, workoutTarget); expect(result.key).to.equal('1 mi @ 1:00:00'); - expect(result.value).to.equal('5:53'); + expect(result.value).to.equal('5:29'); expect(result.pace).to.equal(''); - expect(result.sort).to.be.closeTo(353.07, 0.01); + expect(result.sort).to.be.closeTo(329.48, 0.01); expect(result.result).to.equal('value'); }); diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js @@ -5,7 +5,162 @@ import { defaultTargetSets } from '@/core/targets'; beforeEach(() => { localStorage.clear(); -}) +}); + +test('should load global options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + })); + + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions + .defaultUnitSystem).to.equal('imperial'); +}); + +test('should load input pace from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.pace-calculator-input', JSON.stringify({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + })); + + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + }); +}); + +test('should load local options and target sets from localStorage', async () => { + // Initialize localStorage + const targetSets = { + '_pace_targets': { + name: 'Pace targets #1', + targets: [ + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + ], + }, + 'B': { + name: 'Pace targets #2', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }; + localStorage.setItem('running-tools.pace-calculator-target-sets', JSON.stringify(targetSets)); + localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', + })); + + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ + selectedTargetSet: 'B', + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets) + .to.deep.equal(targetSets); + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) + .to.deep.equal(targetSets.B.targets); +}); + +test('should save global options to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Change default units + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); + + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + })); +}); + +test('should save input pace to localStorage', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Enter input pace data + await wrapper.findComponent({ name: 'pace-input' }).setValue({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + }); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.pace-calculator-input')).to.equal(JSON.stringify({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + })); +}); + +test('should save local options and target sets to localStorage when modified', async () => { + const targetSets = { + '_pace_targets': { + name: 'Pace targets #1', + targets: [ + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + ], + }, + 'B': { + name: 'Pace targets #2', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }; + + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Update target sets and selected target set + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue(targetSets, + 'targetSets'); + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + selectedTargetSet: 'B', + }, 'options'); + + // New selected target set should be saved to localStorage + expect(localStorage.getItem('running-tools.pace-calculator-target-sets')) + .to.equal(JSON.stringify(targetSets)); + expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(JSON.stringify({ + selectedTargetSet: 'B', + })); +}); test('should correctly calculate time results', async () => { // Initialize component @@ -36,7 +191,7 @@ test('should correctly calculate time results', async () => { }); }); -test('should correctly calculate distance results according to default units setting', async () => { +test('should correctly calculate distance results according to global options', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); @@ -48,8 +203,13 @@ test('should correctly calculate distance results according to default units set }); // Set default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); // Get calculate result function const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; @@ -59,8 +219,13 @@ test('should correctly calculate distance results according to default units set expect(result.key).to.equal('1.61 km'); // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); // Assert result is correct result = calculateResult({ type: 'time', time: 600 }); @@ -96,103 +261,10 @@ test('should correctly handle null target set', async () => { .to.deep.equal(paceTargets); }); -test('should load input pace from localStorage', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.pace-calculator-input', JSON.stringify({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - })); - - // Initialize component - const wrapper = shallowMount(PaceCalculator); - - // Assert data loaded - expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - }); -}); - -test('should save input pace to localStorage', async () => { +test('should correctly set AdvancedOptionsInput type prop', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); - // Enter input pace data - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - }); - - // Assert data saved to localStorage - expect(localStorage.getItem('running-tools.pace-calculator-input')).to.equal(JSON.stringify({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - })); -}); - -test('should load options from localStorage', async () => { - // Initialize localStorage - const targetSet2 = { - name: 'Pace targets #2', - targets: [ - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }; - localStorage.setItem('running-tools.pace-calculator-target-sets', JSON.stringify({ - '_pace_targets': { - name: 'Pace targets #1', - targets: [ - { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, - ], - }, - 'B': targetSet2, - })); - localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({ - selectedTargetSet: 'B', - })); - - // Initialize component - const wrapper = shallowMount(PaceCalculator); - - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options) - .to.deep.equal({ selectedTargetSet: 'B' }); - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(targetSet2.targets); -}); - -test('should save options to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(PaceCalculator); - - // Select a new target set - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue({ selectedTargetSet: 'B' }, 'options'); - - // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(JSON.stringify({ - selectedTargetSet: 'B', - })); -}); - -test('should save default units setting to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(PaceCalculator); - - // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); + // Assert type prop is correctly set + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('pace'); }); diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -5,7 +5,167 @@ import { defaultTargetSets } from '@/core/targets'; beforeEach(() => { localStorage.clear(); -}) +}); + +test('should load global options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + })); + + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + }); +}); + +test('should load input race from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.race-calculator-input', JSON.stringify({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + })); + + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + }); +}); + +test('should load local options and target sets from localStorage', async () => { + // Initialize localStorage + const targetSets = { + '_race_targets': { + name: 'Race targets #1', + targets: [ + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + ], + }, + 'B': { + name: 'Race targets #2', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }; + localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify(targetSets)); + localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', + })); + + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ + selectedTargetSet: 'B', + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets) + .to.deep.equal(targetSets); + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) + .to.deep.equal(targetSets.B.targets); +}); + +test('should save global options to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Update options + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + }, 'globalOptions'); + + // New global options should be saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + })); +}); + +test('should save input race to localStorage', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Enter input race data + await wrapper.findComponent({ name: 'pace-input' }).setValue({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + }); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.race-calculator-input')).to.equal(JSON.stringify({ + distanceValue: 1, + distanceUnit: 'miles', + time: 600, + })); +}); + +test('should save local options and target sets to localStorage when modified', async () => { + const targetSets = { + '_race_targets': { + name: 'Race targets #1', + targets: [ + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + ], + }, + 'B': { + name: 'Race targets #2', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }; + + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Update target sets and selected target set + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue(targetSets, + 'targetSets'); + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + selectedTargetSet: 'B', + }, 'options'); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.race-calculator-target-sets')) + .to.equal(JSON.stringify(targetSets)); + expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({ + selectedTargetSet: 'B', + })); +}); test('should correctly predict race times', async () => { // Initialize component @@ -34,45 +194,6 @@ test('should correctly predict race times', async () => { expect(result.sort).to.be.closeTo(2494.80, 0.01); }); -test('should correctly calculate distance results according to default units setting', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Enter input race data - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }); - - // Set default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); - - // Get calculate result function - const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; - - // Assert result is correct - let result = calculateResult({ type: 'time', time: 2495 }); - expect(result.key).to.equal('10.00 km'); - expect(result.value).to.equal('41:35'); - expect(result.pace).to.equal('4:09 / km'); - expect(result.result).to.equal('key'); - expect(result.sort).to.equal(2495); - - // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // Assert result is correct - result = calculateResult({ type: 'time', time: 2495 }); - expect(result.key).to.equal('6.21 mi'); - expect(result.value).to.equal('41:35'); - expect(result.pace).to.equal('6:41 / mi'); - expect(result.result).to.equal('key'); - expect(result.sort).to.equal(2495); -}); - test('should show paces in results table', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); @@ -87,10 +208,6 @@ test('should correctly handle null target set', async () => { // Switch to invalid target set await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, selectedTargetSet: 'does_not_exist', }, 'options'); @@ -99,10 +216,6 @@ test('should correctly handle null target set', async () => { // Switch to valid target set await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { - model: 'AverageModel', - riegelExponent: 1.06, - }, selectedTargetSet: '_race_targets', }, 'options'); @@ -135,7 +248,7 @@ test('should correctly calculate race statistics', async () => { expect(vo2Max).to.equal('V̇O₂ Max: 49.8 ml/kg/min') }); -test('should correctly calculate results according to model options', async () => { +test('should correctly calculate results according to options', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); @@ -146,18 +259,54 @@ test('should correctly calculate results according to model options', async () = time: 1200, }); + // Set default units + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); + + // Get calculate result function + const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; + + // Assert result is correct + let result = calculateResult({ type: 'time', time: 2495 }); + expect(result.key).to.equal('10.00 km'); + expect(result.value).to.equal('41:35'); + expect(result.pace).to.equal('4:09 / km'); + expect(result.result).to.equal('key'); + expect(result.sort).to.equal(2495); + + // Change default units + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', // changed from metric + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); + + // Assert result is correct + result = calculateResult({ type: 'time', time: 2495 }); + expect(result.key).to.equal('6.21 mi'); + expect(result.value).to.equal('41:35'); + expect(result.pace).to.equal('6:41 / mi'); + expect(result.result).to.equal('key'); + expect(result.sort).to.equal(2495); + // Switch model await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { - model: 'RiegelModel', // changed from the Riegel Model + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'RiegelModel', // changed from the Average Model riegelExponent: 1.06, }, - selectedTargetSet: '_race_targets', - }, 'options'); + }, 'globalOptions'); // Calculate result - const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; - let result = calculateResult({ + result = calculateResult({ distanceValue: 10, distanceUnit: 'kilometers', type: 'distance', @@ -168,12 +317,12 @@ test('should correctly calculate results according to model options', async () = // Update Riegel Exponent await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { + defaultUnitSystem: 'imperial', + racePredictionOptions: { model: 'RiegelModel', riegelExponent: 1, // changed from 1.06 }, - selectedTargetSet: '_race_targets', - }, 'options'); + }, 'globalOptions'); // Calculate result result = calculateResult({ @@ -186,121 +335,10 @@ test('should correctly calculate results according to model options', async () = expect(result.value).to.equal('40:00.00'); }); -test('should load input race from localStorage', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.race-calculator-input', JSON.stringify({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - })); - +test('should correctly set AdvancedOptionsInput type prop', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); - // Assert data loaded - expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - }); -}); - -test('should save input race to localStorage', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Enter input race data - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - }); - - // Assert data saved to localStorage - expect(localStorage.getItem('running-tools.race-calculator-input')).to.equal(JSON.stringify({ - distanceValue: 1, - distanceUnit: 'miles', - time: 600, - })); -}); - -test('should save default units setting to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); -}); - -test('should load options from localStorage', async () => { - // Initialize localStorage - const targetSet2 = { - name: 'Race targets #2', - targets: [ - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }; - localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify({ - '_race_targets': { - name: 'Race targets #1', - targets: [ - { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, - ], - }, - 'B': targetSet2, - })); - localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, - selectedTargetSet: 'B', - })); - - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Assert data loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - predictionOptions: { - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }, - selectedTargetSet: 'B', - }); - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(targetSet2.targets); -}); - -test('should save options to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Update options - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - predictionOptions: { - model: 'CameronModel', - riegelExponent: 1.30, - }, - selectedTargetSet: 'B', - }, 'options'); - - // Assert data saved to localStorage - expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({ - predictionOptions: { - model: 'CameronModel', - riegelExponent: 1.3, - }, - selectedTargetSet: 'B', - })); + // Assert type prop is correctly set + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.type).to.equal('race'); }); diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -4,26 +4,31 @@ import SplitCalculator from '@/views/SplitCalculator.vue'; beforeEach(() => { localStorage.clear(); -}) +}); -test('should load selected target set from localStorage', async () => { +test('should load global options from localStorage', async () => { // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ - selectedTargetSet: 'B', + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, })); // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - selectedTargetSet: 'B', - }); + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions + .defaultUnitSystem).to.equal('imperial'); + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.defaultUnitSystem) + .to.equal('imperial'); }); -test('should load targets from localStorage and pass to splitOutputTable', async () => { +test('should load local options and target sets from localStorage', async () => { // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ + const targetSets = { '_split_targets': { name: 'Split targets', targets: [ @@ -40,156 +45,161 @@ test('should load targets from localStorage and pass to splitOutputTable', async { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, ], }, + }; + localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify(targetSets)); + localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', })); // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert default split targets are initially loaded + // Assert data loaded expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - selectedTargetSet: '_split_targets', - }); - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([ - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - ]); - - // Select a new target set - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ selectedTargetSet: 'B', - }, 'options'); - - // Assert new target set is loaded - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([ - { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ]); + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets) + .to.deep.equal(targetSets); + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue) + .to.deep.equal(targetSets.B.targets); }); -test('should correctly handle null target set', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ - selectedTargetSet: 'does_not_exist', - })); - +test('should save global options to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - selectedTargetSet: 'does_not_exist', - }); + // Set default units setting + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); - // Assert empty array passed to SplitOutputTable - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([]); + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'metric', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + })); - // Switch to valid target set + // Update default units setting await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - selectedTargetSet: '_split_targets', - }, 'options'); + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + }, 'globalOptions'); - // Assert non-empty target set passed to SplitOutputTable - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([ - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - ]); + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'AverageModel', + riegelExponent: 1.06, + }, + })); }); -test('should update targets in localStorage when modified by splitOutputTable', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ +test('should save local options and target sets to localStorage when modified', async () => { + const targetSets1 = { '_split_targets': { name: 'Split targets', targets: [ - { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, - { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, ], }, - })); - - // Initialize component - const wrapper = shallowMount(SplitCalculator); - - // Update split times - await wrapper.findComponent({ name: 'split-output-table' }).setValue([ - { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ]); - - // Assert targets correctly saved in localStorage - expect(localStorage.getItem('running-tools.split-calculator-target-sets')).to.equal(JSON.stringify({ - '_split_targets': { - name: 'Split targets', + 'B': { + name: 'Split targets #2', targets: [ { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, ], }, - })); -}); + }; + const targetSets2 = { + '_split_targets': { + name: 'Split targets', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + 'B': { + name: 'Split targets #2', + targets: [ + // split times modified: + { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 185 }, + { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 195 }, + { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 205 }, + ], + }, + }; -test('should save selected target set to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator); - // Select a new target set + // Update target sets and selected target set via AdvancedOptionsInput + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue(targetSets1, + 'targetSets'); await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - selectedTargetSet: '_race_targets', + selectedTargetSet: 'B', }, 'options'); - // New selected target set should be saved to localStorage + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.split-calculator-target-sets')) + .to.equal(JSON.stringify(targetSets1)); expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(JSON.stringify({ - selectedTargetSet: '_race_targets', + selectedTargetSet: 'B', })); -}); - -test('should load default units from localStorage and pass to splitOutputTable', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.default-unit-system', '"metric"'); - - // Initialize component - const wrapper = shallowMount(SplitCalculator); - // Assert default units setting is initialy loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.defaultUnitSystem) - .to.equal('metric'); - - // Assert prop is correct - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.defaultUnitSystem) - .to.equal('metric'); + // Update target sets via SplitOutputTable + await wrapper.findComponent({ name: 'split-output-table' }).setValue(targetSets2.B.targets); - // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // Assert prop is correct - expect(wrapper.findComponent({ name: 'split-output-table' }).vm.defaultUnitSystem) - .to.equal('imperial'); + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.split-calculator-target-sets')) + .to.equal(JSON.stringify(targetSets2)); + expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(JSON.stringify({ + selectedTargetSet: 'B', + })); }); -test('should save default units setting to localStorage when modified', async () => { +test('should correctly handle null target set', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ + selectedTargetSet: 'does_not_exist', + })); + // Initialize component const wrapper = shallowMount(SplitCalculator); - // Set default units setting - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ + selectedTargetSet: 'does_not_exist', + }); - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"metric"'); + // Assert empty array passed to SplitOutputTable + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([]); - // Set default units setting - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); + // Switch to valid target set + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + selectedTargetSet: '_split_targets', + }, 'options'); - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); + // Assert non-empty target set passed to SplitOutputTable + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + ]); }); test('should correctly set AdvancedOptionsInput type prop', async () => { diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js @@ -5,94 +5,29 @@ import { defaultTargetSets } from '@/core/targets'; beforeEach(() => { localStorage.clear(); -}) - -test('should correctly predict workout splits', async () => { - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Enter input race data - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }); - - // Calculate result - const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; - const result = calculateResult({ - splitValue: 1, splitUnit: 'kilometers', - type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', - }); - - // Assert result is correct - expect(result.key).to.equal('1 km @ 10 km'); - expect(result.value).to.equal('4:09.48'); - expect(result.result).to.equal('value'); - expect(result.sort).to.be.closeTo(249.48, 0.01); }); -test('should correctly handle null target set', async () => { - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Switch to invalid target set - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - customTargetNames: false, - model: 'AverageModel', - riegelExponent: 1.06, - selectedTargetSet: 'does_not_exist', - }, 'options'); - - // Assert empty array passed to SingleOutputTable component - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets).to.deep.equal([]); - - // Switch to valid target set - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - customTargetNames: false, - model: 'AverageModel', - riegelExponent: 1.06, - selectedTargetSet: '_workout_targets', - }, 'options'); - - // Assert valid targets passed to SingleOutputTable component - const workoutTargets = defaultTargetSets._workout_targets.targets; - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(workoutTargets); -}); +test('should load global options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.global-options', JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }, + })); -test('should correctly calculate results according to advanced model options', async () => { // Initialize component const wrapper = shallowMount(WorkoutCalculator); - // Enter input race data - await wrapper.findComponent({ name: 'pace-input' }).setValue({ - distanceValue: 5, - distanceUnit: 'kilometers', - time: 1200, - }); - - // Update model and Riegel Exponent - await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ - customTargetNames: false, - predictionOptions: { - model: 'RiegelModel', - riegelExponent: 1.10, + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'PurdyPointsModel', + riegelExponent: 1.2, }, - selectedTargetSet: '_workout_targets', - }, 'options'); - - // Calculate result - const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; - let result = calculateResult({ - customName: 'foo', - splitValue: 1, splitUnit: 'kilometers', - type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', }); - - // Assert result is correct - expect(result.key).to.equal('1 km @ 10 km'); - expect(result.value).to.equal('4:17.23'); }); test('should load input race from localStorage', async () => { @@ -114,6 +49,94 @@ test('should load input race from localStorage', async () => { }); }); +test('should load local options and target sets from localStorage', async () => { + // Initialize localStorage + const targetSets = { + '_workout_targets': { + name: 'Workout targets #1', + targets: [ + { + splitValue: 400, splitUnit: 'meters', + type: 'distance', distanceValue: 1, distanceUnit: 'miles', + }, + { + splitValue: 800, splitUnit: 'meters', + type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', + }, + { + splitValue: 1600, splitUnit: 'meters', + type: 'time', time: 3600, + }, + { + splitValue: 2, splitUnit: 'miles', + type: 'time', time: 7200, + }, + ], + }, + 'B': { + name: 'Workout targets #2', + targets: [ + { + distanceUnit: 'miles', distanceValue: 2, + splitUnit: 'meters', splitValue: 400, + type: 'distance', + }, + { + time: 6000, + splitUnit: 'kilometers', splitValue: 2, + type: 'time', + }, + { + distanceUnit: 'kilometers', distanceValue: 5, + splitUnit: 'miles', splitValue: 1, + type: 'distance' + }, + ], + }, + }; + localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify(targetSets)); + localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + customTargetNames: true, + selectedTargetSet: 'B', + })); + + // Initialize component + const wrapper = shallowMount(WorkoutCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ + customTargetNames: true, + selectedTargetSet: 'B', + }); + expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets) + .to.deep.equal(targetSets); + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) + .to.deep.equal(targetSets.B.targets); +}); + +test('should save global options to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(WorkoutCalculator); + + // Update options + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.3, + }, + }, 'globalOptions'); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.global-options')).to.equal(JSON.stringify({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'CameronModel', + riegelExponent: 1.3, + }, + })); +}); + test('should save input race to localStorage', async () => { // Initialize component const wrapper = shallowMount(WorkoutCalculator); @@ -133,43 +156,8 @@ test('should save input race to localStorage', async () => { })); }); -test('should save default units setting to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Change default units - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('metric', 'defaultUnitSystem'); - await wrapper.findComponent({ name: 'advanced-options-input' }) - .setValue('imperial', 'defaultUnitSystem'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); -}); - -test('should load options from localStorage', async () => { - // Initialize localStorage - const targetSet2 = { - name: 'Workout targets #2', - targets: [ - { - distanceUnit: 'miles', distanceValue: 2, - splitUnit: 'meters', splitValue: 400, - type: 'distance', - }, - { - time: 6000, - splitUnit: 'kilometers', splitValue: 2, - type: 'time', - }, - { - distanceUnit: 'kilometers', distanceValue: 5, - splitUnit: 'miles', splitValue: 1, - type: 'distance' - }, - ], - }; - localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({ +test('should save local options and target sets to localStorage when modified', async () => { + const targetSets = { '_workout_targets': { name: 'Workout targets #1', targets: [ @@ -191,48 +179,146 @@ test('should load options from localStorage', async () => { }, ], }, - 'B': targetSet2, - })); - localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + 'B': { + name: 'Workout targets #2', + targets: [ + { + distanceUnit: 'miles', distanceValue: 2, + splitUnit: 'meters', splitValue: 400, + type: 'distance', + }, + { + time: 6000, + splitUnit: 'kilometers', splitValue: 2, + type: 'time', + }, + { + distanceUnit: 'kilometers', distanceValue: 5, + splitUnit: 'miles', splitValue: 1, + type: 'distance' + }, + ], + }, + }; + + // Initialize component + const wrapper = shallowMount(WorkoutCalculator); + + // Update target sets, selected target set, and target name customization + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue(targetSets, + 'targetSets'); + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + customTargetNames: true, + selectedTargetSet: 'B', + }, 'options'); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.workout-calculator-target-sets')) + .to.equal(JSON.stringify(targetSets)); + expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({ customTargetNames: true, - model: 'PurdyPointsModel', - riegelExponent: 1.2, selectedTargetSet: 'B', })); +}); +test('should correctly predict workout splits', async () => { // Initialize component const wrapper = shallowMount(WorkoutCalculator); - // Assert data loaded - expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({ - customTargetNames: true, - model: 'PurdyPointsModel', - riegelExponent: 1.2, - selectedTargetSet: 'B', + // Enter input race data + await wrapper.findComponent({ name: 'pace-input' }).setValue({ + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }); + + // Calculate result + const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; + const result = calculateResult({ + splitValue: 1, splitUnit: 'kilometers', + type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', }); + + // Assert result is correct + expect(result.key).to.equal('1 km @ 10 km'); + expect(result.value).to.equal('4:09.48'); + expect(result.result).to.equal('value'); + expect(result.sort).to.be.closeTo(249.48, 0.01); +}); + +test('should correctly handle null target set', async () => { + // Initialize component + const wrapper = shallowMount(WorkoutCalculator); + + // Switch to invalid target set + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + customTargetNames: false, + selectedTargetSet: 'does_not_exist', + }, 'options'); + + // Assert empty array passed to SingleOutputTable component + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets).to.deep.equal([]); + + // Switch to valid target set + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + customTargetNames: false, + selectedTargetSet: '_workout_targets', + }, 'options'); + + // Assert valid targets passed to SingleOutputTable component + const workoutTargets = defaultTargetSets._workout_targets.targets; expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(targetSet2.targets); + .to.deep.equal(workoutTargets); }); -test('should save options to localStorage when modified', async () => { +test('should correctly calculate results according to options', async () => { // Initialize component const wrapper = shallowMount(WorkoutCalculator); - // Update options + // Enter input race data + await wrapper.findComponent({ name: 'pace-input' }).setValue({ + distanceValue: 5, + distanceUnit: 'kilometers', + time: 1200, + }); + + // Update model and Riegel exponent + await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ + defaultUnitSystem: 'imperial', + racePredictionOptions: { + model: 'RiegelModel', + riegelExponent: 1.10, + }, + }, 'globalOptions'); + + // Calculate result + const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult; + let result = calculateResult({ + customName: 'foo', + splitValue: 1, splitUnit: 'kilometers', + type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', + }); + + // Assert result is correct + expect(result.key).to.equal('1 km @ 10 km'); + expect(result.value).to.equal('4:17.23'); + + // Update target name customization await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({ customTargetNames: true, - model: 'CameronModel', - riegelExponent: 1.3, - selectedTargetSet: 'B', + selectedTargetSet: '_workout_targets', }, 'options'); - // Assert data saved to localStorage - expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({ - customTargetNames: true, - model: 'CameronModel', - riegelExponent: 1.3, - selectedTargetSet: 'B', - })); + // Calculate result + result = calculateResult({ + customName: 'foo', + splitValue: 1, splitUnit: 'kilometers', + type: 'distance', distanceValue: 10, distanceUnit: 'kilometers', + }); + + // Assert result is correct + expect(result.key).to.equal('foo'); + expect(result.value).to.equal('4:17.23'); }); test('should correctly set AdvancedOptionsInput type prop', async () => {