running-tools

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

commit 3ebace2a5a49947c003efcea22c3e8be79b0e031
parent ab7065f6464966d494328bb514b3327cdebb4f64
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat,  5 Jul 2025 20:37:26 -0700

Move *-target-set settings into *-options setting

Diffstat:
Msrc/utils/calculators.ts | 56+++++++++++++++++++++++++++++++-------------------------
Msrc/utils/storage.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/views/BatchCalculator.vue | 76+++++++++++++++++++---------------------------------------------------------
Msrc/views/PaceCalculator.vue | 10+++++++---
Msrc/views/RaceCalculator.vue | 13+++++--------
Msrc/views/SplitCalculator.vue | 17++++++++++-------
Msrc/views/WorkoutCalculator.vue | 11++++-------
Mtests/e2e/cross-calculator.spec.js | 347++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mtests/unit/views/BatchCalculator.spec.js | 111+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtests/unit/views/PaceCalculator.spec.js | 13++++++++-----
Mtests/unit/views/RaceCalculator.spec.js | 71++++++++++++++++++++++++++++-------------------------------------------
Mtests/unit/views/SplitCalculator.spec.js | 16+++++++++++-----
Mtests/unit/views/WorkoutCalculator.spec.js | 75+++++++++++++++++++++++++++++++++------------------------------------------
13 files changed, 571 insertions(+), 308 deletions(-)

diff --git a/src/utils/calculators.ts b/src/utils/calculators.ts @@ -7,48 +7,54 @@ import { DistanceUnits, DistanceUnitData, UnitSystems, convertDistance, getDefaultDistanceUnit } from '@/utils/units'; import type { DistanceTime } from '@/utils/units'; -/* - * The two possible result fields of a target result: "key" and "value" - */ -export enum ResultType { - Key = 'key', - Value = 'value', -}; +export enum Calculators { + Pace, + Race, + Split, + Workout, +} /* - * The type for target results + * The type for the available race statistics */ -export interface TargetResult { - key: string, - value: string, - pace: string, - result: ResultType, - sort: number, +export interface RaceStats { + purdyPoints: number, + vo2Max: number, + vo2: number, + vo2MaxPercentage: number, }; /* - * The type for the options specific to the race calculator + * The type for the options specific to each calculator */ -export interface RaceOptions { +export interface StandardOptions { + selectedTargetSet: string, +} +export interface RaceOptions extends StandardOptions { model: raceUtils.RacePredictionModel, riegelExponent: number, }; +export interface WorkoutOptions extends RaceOptions { + customTargetNames: boolean, +}; /* - * The type for the available race statistics + * The two possible result fields of a target result: "key" and "value" */ -export interface RaceStats { - purdyPoints: number, - vo2Max: number, - vo2: number, - vo2MaxPercentage: number, +export enum ResultType { + Key = 'key', + Value = 'value', }; /* - * The type for the options specific to the workout calculator + * The type for target results */ -export interface WorkoutOptions extends RaceOptions { - customTargetNames: boolean, +export interface TargetResult { + key: string, + value: string, + pace: string, + result: ResultType, + sort: number, }; /** diff --git a/src/utils/storage.ts b/src/utils/storage.ts @@ -1,3 +1,5 @@ +import { RacePredictionModel } from '@/utils/races'; + // The global localStorage prefix const prefix = 'running-tools'; @@ -24,14 +26,67 @@ export function set<Type>(key: string, value: Type) { } /** + * Delete a localStorage item + * @param {string} key The localStorage item's key + */ +export function unset(key: string) { + localStorage.removeItem(`${prefix}.${key}`); +} + +/** * Migrate outdated localStorage options */ export function migrate() { - // Add customTargetNames property to workout options (>1.4.1) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const workoutOptions = get('workout-calculator-options') as any; // TODO: update types - if (workoutOptions !== null && workoutOptions.customTargetNames === undefined) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + + // Move pace-calculator-target-set into new pace-calculator-options (>1.4.1) + const paceSelectedTargetSet = get<string>('pace-calculator-target-set'); + if (paceSelectedTargetSet !== null) { + const paceOptions = { selectedTargetSet: paceSelectedTargetSet }; + set('pace-calculator-options', paceOptions); + unset('pace-calculator-target-set'); + } + + // Move race-calculator-target-set into race-calculator-options (>1.4.1) + const raceSelectedTargetSet = get<string>('race-calculator-target-set'); + const raceOptions = get<any>('race-calculator-options') || { + model: RacePredictionModel.AverageModel, + riegelExponent: 1.06, + selectedTargetSet: '_race_targets', + }; + if (raceSelectedTargetSet !== null) { + raceOptions.selectedTargetSet = raceSelectedTargetSet; + set('race-calculator-options', raceOptions); + unset('race-calculator-target-set'); + } + + // Move split-calculator-target-set into new split-calculator-options (>1.4.1) + const splitSelectedTargetSet = get<string>('split-calculator-target-set'); + if (splitSelectedTargetSet !== null) { + const splitOptions = { selectedTargetSet: splitSelectedTargetSet }; + set('split-calculator-options', splitOptions); + unset('split-calculator-target-set'); + } + + // Move workout-calculator-target-set into workout-calculator-options (>1.4.1) + const workoutSelectedTargetSet = get<string>('workout-calculator-target-set'); + const workoutOptions = get<any>('workout-calculator-options') || { + customTargetNames: false, + model: RacePredictionModel.AverageModel, + riegelExponent: 1.06, + selectedTargetSet: '_workout_targets', + }; + if (workoutSelectedTargetSet !== null) { + workoutOptions.selectedTargetSet = workoutSelectedTargetSet; + set('workout-calculator-options', workoutOptions); + unset('workout-calculator-target-set'); + } + + // Add customTargetNames property to workout-calculator-options (>1.4.1) + if (workoutOptions.customTargetNames === undefined) { workoutOptions.customTargetNames = false; set('workout-calculator-options', workoutOptions); } + + /* eslint-enable @typescript-eslint/no-explicit-any */ } diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue @@ -36,29 +36,30 @@ </div> <div> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="calcOptions.selectedTargetSet" :set-type="options.calculator === BatchCompatableCalculators.Workout ? targetUtils.TargetSetTypes.Workout : targetUtils.TargetSetTypes.Standard" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem" :customWorkoutNames="options.calculator === BatchCompatableCalculators.Workout ? - (advancedOptions as WorkoutOptions).customTargetNames : false"/> + (calcOptions as WorkoutOptions).customTargetNames : false"/> </div> <div v-if="options.calculator === 'workout'"> Target Name Customization: - <select v-model="(advancedOptions as WorkoutOptions).customTargetNames" + <select v-model="(calcOptions as WorkoutOptions).customTargetNames" aria-label="Target name customization"> <option :value="false">Disabled</option> <option :value="true">Enabled</option> </select> </div> <race-options-input v-if="options.calculator !== BatchCompatableCalculators.Pace" - v-model="advancedOptions as RaceOptions"/> + v-model="calcOptions as RaceOptions"/> </details> <h2>Batch Results</h2> <double-output-table class="output" :input-times="inputTimes" :input-distance="inputDistance" :calculate-result="calculateResult" - :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/> + :targets="targetSets[calcOptions.selectedTargetSet] ? + targetSets[calcOptions.selectedTargetSet].targets : []"/> </div> </template> @@ -66,7 +67,8 @@ import { computed } from 'vue'; import * as calcUtils from '@/utils/calculators'; -import type { RaceOptions, TargetResult, WorkoutOptions } from '@/utils/calculators'; +import type { RaceOptions, StandardOptions, TargetResult, + WorkoutOptions } from '@/utils/calculators'; import { RacePredictionModel } from '@/utils/races'; import * as targetUtils from '@/utils/targets'; import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units'; @@ -123,14 +125,6 @@ const options = useStorage<BatchCalculatorOptions>('batch-calculator-options', { const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); /* - * The current selected target sets for each calculator - */ -const selectedPaceTargetSet = useStorage<string>('pace-calculator-target-set', '_pace_targets'); -const selectedRaceTargetSet = useStorage<string>('race-calculator-target-set', '_race_targets'); -const selectedWorkoutTargetSet = useStorage<string>('workout-calculator-target-set', - '_workout_targets'); - -/* * The target sets for each calculator */ const paceTargetSets = useStorage<targetUtils.StandardTargetSets>('pace-calculator-target-sets', { @@ -144,16 +138,21 @@ const workoutTargetSets = useStorage<targetUtils.WorkoutTargetSets>('workout-cal }); /* - * The advanced options for each calculator + * The options for each calculator */ +const paceOptions = useStorage<StandardOptions>('pace-calculator-options', { + selectedTargetSet: '_pace_targets', +}); const raceOptions = useStorage<RaceOptions>('race-calculator-options', { model: RacePredictionModel.AverageModel, riegelExponent: 1.06, + selectedTargetSet: '_race_targets', }); const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options', { customTargetNames: false, model: RacePredictionModel.AverageModel, riegelExponent: 1.06, + selectedTargetSet: '_workout_targets', }); /* @@ -176,43 +175,6 @@ const inputTimes = computed<Array<number>>(() => { }); /* - * The selected target set for the current calculator - */ -const selectedTargetSet = computed<string>({ - get: (): string => { - switch (options.value.calculator) { - case (BatchCompatableCalculators.Pace): { - return selectedPaceTargetSet.value; - } - case (BatchCompatableCalculators.Race): { - return selectedRaceTargetSet.value; - } - default: - case (BatchCompatableCalculators.Workout): { - return selectedWorkoutTargetSet.value; - } - } - }, - set: (newValue: string) => { - switch (options.value.calculator) { - case (BatchCompatableCalculators.Pace): { - selectedPaceTargetSet.value = newValue; - break; - } - case (BatchCompatableCalculators.Race): { - selectedRaceTargetSet.value = newValue; - break; - } - default: - case (BatchCompatableCalculators.Workout): { - selectedWorkoutTargetSet.value = newValue; - break; - } - } - }, -}); - -/* * The target sets for the current calculator */ const targetSets = computed<targetUtils.TargetSets>({ @@ -250,13 +212,13 @@ const targetSets = computed<targetUtils.TargetSets>({ }); /* - * The advanced options for the current calculator + * The options for the current calculator */ -const advancedOptions = computed<null | RaceOptions | WorkoutOptions>({ +const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({ get: () => { switch (options.value.calculator) { case (BatchCompatableCalculators.Pace): { - return null; + return paceOptions.value; } case (BatchCompatableCalculators.Race): { return raceOptions.value; @@ -267,10 +229,10 @@ const advancedOptions = computed<null | RaceOptions | WorkoutOptions>({ } } }, - set: (newValue: null | RaceOptions | WorkoutOptions) => { + set: (newValue: StandardOptions | RaceOptions | WorkoutOptions) => { switch(options.value.calculator) { case (BatchCompatableCalculators.Pace): { - // do nothing + paceOptions.value = newValue as StandardOptions; break; } case (BatchCompatableCalculators.Race): { diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -18,7 +18,7 @@ </div> <div> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="options.selectedTargetSet" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> </details> @@ -26,12 +26,14 @@ <h2>Equivalent Paces</h2> <single-output-table class="output" :calculate-result="x => calculatePaceResults(input, x, defaultUnitSystem, true)" - :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/> + :targets="targetSets[options.selectedTargetSet] ? + targetSets[options.selectedTargetSet].targets : []"/> </div> </template> <script setup lang="ts"> import { calculatePaceResults } from '@/utils/calculators'; +import type { StandardOptions } from '@/utils/calculators'; import { defaultTargetSets } from '@/utils/targets'; import type { StandardTargetSets } from '@/utils/targets'; import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units'; @@ -60,7 +62,9 @@ const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectD /* * The current selected target set */ -const selectedTargetSet = useStorage<string>('pace-calculator-target-set', '_pace_targets'); +const options = useStorage<StandardOptions>('pace-calculator-options', { + selectedTargetSet: '_pace_targets', +}); /* * The target sets diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -35,7 +35,7 @@ </div> <div> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="options.selectedTargetSet" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> <race-options-input v-model="options"/> @@ -44,7 +44,8 @@ <h2>Equivalent Race Results</h2> <single-output-table class="output" show-pace :calculate-result="x => calculateRaceResults(input, x, options, defaultUnitSystem, true)" - :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/> + :targets="targetSets[options.selectedTargetSet] ? + targetSets[options.selectedTargetSet].targets : []"/> </div> </template> @@ -82,19 +83,15 @@ const input = useStorage<DistanceTime>('race-calculator-input', { const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); /* -* The race prediction options +* The race calculator options */ const options = useStorage<RaceOptions>('race-calculator-options', { model: RacePredictionModel.AverageModel, riegelExponent: 1.06, + selectedTargetSet: '_race_targets', }); /* - * The current selected target set - */ -const selectedTargetSet = useStorage<string>('race-calculator-target-set', '_race_targets'); - -/* * The target sets */ const targetSets = useStorage<StandardTargetSets>('race-calculator-target-sets', { diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -11,7 +11,7 @@ <div class="target-set"> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="options.selectedTargetSet" :set-type="TargetSetTypes.Split" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> @@ -26,6 +26,7 @@ <script setup lang="ts"> import { computed } from 'vue'; +import type { StandardOptions } from '@/utils/calculators'; import { defaultTargetSets } from '@/utils/targets'; import { TargetSetTypes } from '@/utils/targets'; import type { SplitTargetSet, SplitTargetSets } from '@/utils/targets'; @@ -42,9 +43,11 @@ import useStorage from '@/composables/useStorage'; const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem()); /* - * The current selected target set + * The split calculator options */ -const selectedTargetSet = useStorage<string>('split-calculator-target-set', '_split_targets'); +const options = useStorage<StandardOptions>('split-calculator-options', { + selectedTargetSet: '_split_targets' +}); /* * The default output targets @@ -58,15 +61,15 @@ const targetSets = useStorage<SplitTargetSets>('split-calculator-target-sets', { */ const targetSet = computed({ get: () => { - if (targetSets.value[selectedTargetSet.value]) { - return targetSets.value[selectedTargetSet.value].targets + if (targetSets.value[options.value.selectedTargetSet]) { + return targetSets.value[options.value.selectedTargetSet].targets } else { return [] } }, set: (newValue) => { - if (targetSets.value[selectedTargetSet.value]) { - targetSets.value[selectedTargetSet.value].targets = newValue; + if (targetSets.value[options.value.selectedTargetSet]) { + targetSets.value[options.value.selectedTargetSet].targets = newValue; } }, }); diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue @@ -18,7 +18,7 @@ </div> <div> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="options.selectedTargetSet" :set-type="TargetSetTypes.Workout" :customWorkoutNames="options.customTargetNames" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> @@ -35,7 +35,8 @@ <h2>Workout Splits</h2> <single-output-table class="output" :calculate-result="x => calculateWorkoutResults(input, x as WorkoutTarget, options, true)" - :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/> + :targets="targetSets[options.selectedTargetSet] ? + targetSets[options.selectedTargetSet].targets : []"/> </div> </template> @@ -76,14 +77,10 @@ const options = useStorage<WorkoutOptions>('workout-calculator-options', { customTargetNames: false, model: RacePredictionModel.AverageModel, riegelExponent: 1.06, + selectedTargetSet: '_workout_targets', }); /* - * The current selected target set - */ -const selectedTargetSet = useStorage<string>('workout-calculator-target-set', '_workout_targets'); - -/* * The target sets */ const targetSets = useStorage<WorkoutTargetSets>('workout-calculator-target-sets', { diff --git a/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js @@ -216,7 +216,7 @@ test('Cross-calculator', async ({ page }) => { await expect(page.getByRole('row')).toHaveCount(5); // Assert general localStorage entries are correct - expect(await page.evaluate(() => localStorage.length)).toEqual(18); + expect(await page.evaluate(() => localStorage.length)).toEqual(16); expect(await page.evaluate(() => localStorage.getItem('running-tools.default-unit-system'))) .toEqual(JSON.stringify('metric')); @@ -242,54 +242,54 @@ test('Cross-calculator', async ({ page }) => { time: 930, })); const paceCalculatorKey = parseInt(JSON.parse(await page.evaluate(() => - localStorage.getItem('running-tools.pace-calculator-target-set')))); + localStorage.getItem('running-tools.pace-calculator-options'))).selectedTargetSet); expect(paceCalculatorKey - parseInt(Date.now().toString())).toBeLessThan(100000); - expect(await page.evaluate(() => localStorage.getItem('running-tools.pace-calculator-target-sets'))) - .toEqual(JSON.stringify({ - _pace_targets: { - name: 'Common Pace Targets', - targets: [ - { type: 'distance', distanceValue: 100, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 300, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 600, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 6, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 8, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 10, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, - { type: 'time', time: 600 }, - { type: 'time', time: 1800 }, - { type: 'time', time: 3600 }, - ], - }, - [paceCalculatorKey.toString()]: { - name: '800m Splits', - targets: [ - { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, - { type: 'time', time: 600 }, - ], - }, - })); + expect(await page.evaluate(() => + localStorage.getItem('running-tools.pace-calculator-target-sets'))).toEqual(JSON.stringify({ + _pace_targets: { + name: 'Common Pace Targets', + targets: [ + { type: 'distance', distanceValue: 100, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 300, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 600, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 6, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 8, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 10, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, + { type: 'time', time: 600 }, + { type: 'time', time: 1800 }, + { type: 'time', time: 3600 }, + ], + }, + [paceCalculatorKey.toString()]: { + name: '800m Splits', + targets: [ + { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'time', time: 600 }, + ], + }, + })); // Assert localStorage entries for the race calculator are correct expect(await page.evaluate(() => @@ -302,11 +302,9 @@ test('Cross-calculator', async ({ page }) => { localStorage.getItem('running-tools.race-calculator-options'))).toEqual(JSON.stringify({ model: 'RiegelModel', riegelExponent: 1.06, + selectedTargetSet: '_race_targets', })); expect(await page.evaluate(() => - localStorage.getItem('running-tools.race-calculator-target-set'))) - .toEqual(JSON.stringify('_race_targets')); - expect(await page.evaluate(() => localStorage.getItem('running-tools.race-calculator-target-sets'))).toEqual(JSON.stringify({ _race_targets: { name: 'Common Race Targets', @@ -333,8 +331,10 @@ test('Cross-calculator', async ({ page }) => { // Assert localStorage entries for the split calculator are correct expect(await page.evaluate(() => - localStorage.getItem('running-tools.split-calculator-target-set'))) - .toEqual(JSON.stringify('_split_targets')); + localStorage.getItem('running-tools.split-calculator-options'))) + .toEqual(JSON.stringify({ + selectedTargetSet: '_split_targets', + })); expect(await page.evaluate(() => localStorage.getItem('running-tools.split-calculator-target-sets'))).toEqual(JSON.stringify({ _split_targets: { @@ -381,11 +381,9 @@ test('Cross-calculator', async ({ page }) => { customTargetNames: true, model: 'VO2MaxModel', riegelExponent: 1.06, + selectedTargetSet: '_workout_targets', })); expect(await page.evaluate(() => - localStorage.getItem('running-tools.workout-calculator-target-set'))) - .toEqual(JSON.stringify('_workout_targets')); - expect(await page.evaluate(() => localStorage.getItem('running-tools.workout-calculator-target-sets'))).toEqual(JSON.stringify({ _workout_targets: { name: 'Common Workout Targets', @@ -508,27 +506,250 @@ test('Cross-calculator', async ({ page }) => { test('v1.4.1 Migration', async ({ page }) => { // Structure: // - Set v1.4.1 localStorage entries - // - Reload app and assert the proper migrations were performed + // - Reload app + // - Assert the proper localStorage migrations were performed + // - Assert UI options are up to date - // Set v1.4.1 localStorage options + // Set general localStorage entries await page.goto('/'); - await page.evaluate(() => - localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ - // No customTargetNames property + await page.evaluate(() => localStorage.setItem('running-tools.default-unit-system', + JSON.stringify('metric'))); + + // Set batch calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.batch-calculator-input', + JSON.stringify({ + distanceValue: 2, + distanceUnit: 'miles', + time: 630, + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.batch-calculator-options', + JSON.stringify({ + calculator: 'race', + increment: 10, + rows: 15, + }) + )); + + // Set pace calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-input', + JSON.stringify({ + distanceValue: 2, + distanceUnit: 'miles', + time: 930, + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-target-set', + JSON.stringify('123456789'))); // Property moved after v1.4.1 + await page.evaluate(() => localStorage.setItem('running-tools.pace-calculator-target-sets', + JSON.stringify({ + _pace_targets: { + name: 'Common Pace Targets', + targets: [ + { type: 'distance', distanceValue: 100, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 300, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 600, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1000, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 4, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 6, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 8, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 10, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, + { type: 'time', time: 600 }, + { type: 'time', time: 1800 }, + { type: 'time', time: 3600 }, + ], + }, + '123456789': { + name: '800m Splits', + targets: [ + { type: 'distance', distanceValue: 0.4, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'time', time: 600 }, + ], + }, + }) + )); + + // Set race calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-input', + JSON.stringify({ + distanceValue: 2, + distanceUnit: 'miles', + time: 630, + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-options', + JSON.stringify({ + model: 'RiegelModel', + riegelExponent: 1.06, + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-target-set', + JSON.stringify('_race_targets'))); // Property moved after v1.4.1 + await page.evaluate(() => localStorage.setItem('running-tools.race-calculator-target-sets', + JSON.stringify({ + _race_targets: { + name: 'Common Race Targets', + targets: [ + { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1600, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 3200, distanceUnit: 'meters' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 6, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 8, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 10, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 15, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, + { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, + ], + }, + }) + )); + + // Set split calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.split-calculator-target-set', + JSON.stringify('_split_targets'))); // Property moved after v1.4.1 + await page.evaluate(() => localStorage.setItem('running-tools.split-calculator-target-sets', + JSON.stringify({ + _split_targets: { + name: '5K 1600m Splits', + targets: [ + { type: 'distance', distanceValue: 1.6, distanceUnit: 'kilometers', splitTime: 420 }, + { type: 'distance', distanceValue: 3.2, distanceUnit: 'kilometers', splitTime: 390 }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers', splitTime: 390 }, + ], + }, + }) + )); + + // Set unit calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.unit-calculator-category', + JSON.stringify('speed_and_pace'))); + await page.evaluate(() => localStorage.setItem('running-tools.unit-calculator-inputs', + JSON.stringify({ + distance: { + inputValue: 1, + inputUnit: 'miles', + outputUnit: 'kilometers', + }, + time: { + inputValue: 1, + inputUnit: 'seconds', + outputUnit: 'hh:mm:ss', + }, + speed_and_pace: { + inputValue: 10, + inputUnit: 'kilometers_per_hour', + outputUnit: 'seconds_per_mile', + }, + }) + )); + + // Set workout calculator localStorage entries + await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-input', + JSON.stringify({ + distanceValue: 1, + distanceUnit: 'miles', + time: 301, + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-options', + JSON.stringify({ + // customTargetNames property added after v1.4.1 model: 'VO2MaxModel', riegelExponent: 1.06, - }))); + }) + )); + await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-target-set', + JSON.stringify('_workout_targets'))); // Property moved after v1.4.1 + await page.evaluate(() => localStorage.setItem('running-tools.workout-calculator-target-sets', + JSON.stringify({ + _workout_targets: { + name: 'Common Workout Targets', + 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: 1, splitUnit: 'miles', + type: 'distance', distanceValue: 1, distanceUnit: 'marathons', + }, + ], + }, + }) + )); // Reload the app and assert localStorage is updated await page.goto('/'); + expect(await page.evaluate(() => localStorage.getItem('running-tools.pace-calculator-options'))) + .toEqual(JSON.stringify({ + selectedTargetSet: '123456789', + })); + expect(await page.evaluate(() => + localStorage.getItem('running-tools.pace-calculator-target-set'))).toBeNull(); + 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(() => + localStorage.getItem('running-tools.race-calculator-target-set'))).toBeNull(); + expect(await page.evaluate(() => localStorage.getItem('running-tools.split-calculator-options'))) + .toEqual(JSON.stringify({ + selectedTargetSet: '_split_targets', + })); + expect(await page.evaluate(() => + localStorage.getItem('running-tools.split-calculator-target-set'))).toBeNull(); expect(await page.evaluate(() => localStorage.getItem('running-tools.workout-calculator-options'))).toEqual(JSON.stringify({ model: 'VO2MaxModel', riegelExponent: 1.06, + selectedTargetSet: '_workout_targets', customTargetNames: false, })); + expect(await page.evaluate(() => + localStorage.getItem('running-tools.workout-calculator-target-set'))).toBeNull(); - // Assert target name customization is disabled by default + // Assert migrated settings are correctly initialized and/or loaded + await page.getByRole('button', { name: 'Pace Calculator' }).click(); + await page.getByText('Advanced Options').click(); + await expect(page.getByLabel('Selected target set')).toHaveValue('123456789'); + await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Workout Calculator' }).click(); await page.getByText('Advanced Options').click(); await expect(page.getByLabel('Target name customization')).toHaveValue('false'); diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js @@ -123,7 +123,7 @@ test('should save default units setting from localStorage when modified', async expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); }); -test('should load selected target set from localStorage', async () => { +test('should load calculator options from localStorage', async () => { // Initialize localStorage const selectedTargetSets = [ { @@ -157,7 +157,6 @@ test('should load selected target set from localStorage', async () => { ], } })); - localStorage.setItem('running-tools.pace-calculator-target-set', '"A"'); localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify({ 'C': selectedTargetSets[1], 'D': { @@ -167,7 +166,6 @@ test('should load selected target set from localStorage', async () => { ], } })); - localStorage.setItem('running-tools.race-calculator-target-set', '"C"'); localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({ 'E': selectedTargetSets[2], 'F': { @@ -177,31 +175,57 @@ test('should load selected target set from localStorage', async () => { ], } })); - localStorage.setItem('running-tools.workout-calculator-target-set', '"E"'); + localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({ + selectedTargetSet: 'A', + })); + localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + model: 'PurdyPointsModel', + riegelExponent: 1.2, + selectedTargetSet: 'C', + })); + localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + customTargetNames: true, + model: 'RiegelModel', + riegelExponent: 1.1, + selectedTargetSet: 'E', + })); // Initialize component const wrapper = shallowMount(BatchCalculator); - // Assert selected pace target set is loaded + // Assert pace calculator options are loaded await wrapper.find('select[aria-label="Calculator"]').setValue('pace'); expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('A'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[0].targets); - // Assert selected race target set is loaded + // Assert race calculator options are loaded await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ + model: 'PurdyPointsModel', + riegelExponent: 1.2, + selectedTargetSet: 'C', + }); expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('C'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[1].targets); - // Assert selected workout target set is loaded + // Assert workout calculator options are loaded await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); + expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ + customTargetNames: true, + model: 'RiegelModel', + riegelExponent: 1.1, + selectedTargetSet: 'E', + }); + expect(wrapper.find('select[aria-label="Target name customization"]').element.value) + .to.equal('true'); expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('E'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[2].targets); }); -test('should save selected target set to localStorage when modified', async () => { +test('should save calculator options to localStorage when modified', async () => { // Initialize localStorage const selectedTargetSets = [ { @@ -235,7 +259,9 @@ test('should save selected target set to localStorage when modified', async () = ], } })); - localStorage.setItem('running-tools.pace-calculator-target-set', '"B"'); + localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', + })); localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify({ 'C': selectedTargetSets[1], 'D': { @@ -245,7 +271,11 @@ test('should save selected target set to localStorage when modified', async () = ], } })); - localStorage.setItem('running-tools.race-calculator-target-set', '"D"'); + localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + model: 'AverageModel', + riegelExponent: 1.06, + selectedTargetSet: 'D', + })); localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({ 'E': selectedTargetSets[2], 'F': { @@ -255,60 +285,61 @@ test('should save selected target set to localStorage when modified', async () = ], } })); - localStorage.setItem('running-tools.workout-calculator-target-set', '"F"'); + localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + customWorkoutNames: false, + model: 'AverageModel', + riegelExponent: 1.06, + selectedTargetSet: 'F', + })); // Initialize component const wrapper = shallowMount(BatchCalculator); - // Update selected pace target set + // Update pace calculator options X await wrapper.find('select[aria-label="Calculator"]').setValue('pace'); await wrapper.findComponent({ name: 'target-set-selector' }).setValue('A', 'selectedTargetSet'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[0].targets); - expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal('"A"'); - // Assert selected race target set is loaded + // Update race calculator options await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ + model: 'PurdyPointsModel', + riegelExponent: 1.2, + selectedTargetSet: 'D', + }); await wrapper.findComponent({ name: 'target-set-selector' }).setValue('C', 'selectedTargetSet'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[1].targets); - expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal('"C"'); - // Assert selected workout target set is loaded + // Update workout calculator options await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); + await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ + customTargetNames: false, + model: 'RiegelModel', + riegelExponent: 1.1, + selectedTargetSet: 'F', + }); + wrapper.find('select[aria-label="Target name customization"]').setValue('true'); await wrapper.findComponent({ name: 'target-set-selector' }).setValue('E', 'selectedTargetSet'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[2].targets); - expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal('"E"'); -}); -test('should load advanced model options from localStorage', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + // Assert options saved to localStorage + expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(JSON.stringify({ + selectedTargetSet: 'A', + })); + expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({ model: 'PurdyPointsModel', riegelExponent: 1.2, + selectedTargetSet: 'C', })); - localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({ + customTargetNames: true, model: 'RiegelModel', riegelExponent: 1.1, + selectedTargetSet: 'E', })); - - // Initialize component - const wrapper = shallowMount(BatchCalculator); - - // Assert race prediction options are loaded - await wrapper.find('select[aria-label="Calculator"]').setValue('race'); - expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ - model: 'PurdyPointsModel', - riegelExponent: 1.2, - }); - - // Assert workout prediction options are loaded - await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); - expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ - model: 'RiegelModel', - riegelExponent: 1.1, - }); }); test('should pass correct input props to DoubleOutputTable', async () => { @@ -371,10 +402,12 @@ test('should pass correct input props to DoubleOutputTable', async () => { test('should correctly calculate outputs', async () => { // Initialize localStorage localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + selectedTargetSet: '_race_targets', model: 'PurdyPointsModel', riegelExponent: 1.2, })); localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + selectedTargetSet: '_workout_targets', model: 'RiegelModel', riegelExponent: 1.1, })); diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js @@ -132,7 +132,7 @@ test('should save input pace to localStorage', async () => { })); }); -test('should load selected target set from localStorage', async () => { +test('should load options from localStorage', async () => { // Initialize localStorage const targetSet2 = { name: 'Pace targets #2', @@ -153,7 +153,9 @@ test('should load selected target set from localStorage', async () => { }, 'B': targetSet2, })); - localStorage.setItem('running-tools.pace-calculator-target-set', '"B"'); + localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', + })); // Initialize component const wrapper = shallowMount(PaceCalculator); @@ -165,7 +167,7 @@ test('should load selected target set from localStorage', async () => { .to.deep.equal(targetSet2.targets); }); -test('should save selected target set to localStorage when modified', async () => { +test('should save options to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); @@ -174,8 +176,9 @@ test('should save selected target set to localStorage when modified', async () = .setValue('B', 'selectedTargetSet'); // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.pace-calculator-target-set')) - .to.equal('"B"'); + 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 () => { diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -123,7 +123,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 advanced model options', async () => { +test('should correctly calculate results according to model options', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); @@ -138,6 +138,7 @@ test('should correctly calculate results according to advanced model options', a await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ model: 'RiegelModel', riegelExponent: 1.06, // default value + selectedTargetSet: '_race_targets', }); // Calculate result @@ -155,6 +156,7 @@ test('should correctly calculate results according to advanced model options', a await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ model: 'RiegelModel', // existing value riegelExponent: 1, + selectedTargetSet: '_race_targets', }); // Calculate result @@ -206,7 +208,19 @@ test('should save input race to localStorage', async () => { })); }); -test('should load selected target set from localStorage', async () => { +test('should save default units setting to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // 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', @@ -227,48 +241,10 @@ test('should load selected target set from localStorage', async () => { }, 'B': targetSet2, })); - localStorage.setItem('running-tools.race-calculator-target-set', '"B"'); - - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) - .to.equal('B'); - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(targetSet2.targets); -}); - -test('should save selected target set to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Select a new target set - await wrapper.findComponent({ name: 'target-set-selector' }) - .setValue('B', 'selectedTargetSet'); - - // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.race-calculator-target-set')) - .to.equal('"B"'); -}); - -test('should save default units setting to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(RaceCalculator); - - // Change default units - await wrapper.find('select[aria-label="Default units"]').setValue('metric'); - await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); -}); - -test('should load advanced model options from localStorage', async () => { - // Initialize localStorage localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ model: 'PurdyPointsModel', riegelExponent: 1.2, + selectedTargetSet: 'B', })); // Initialize component @@ -278,23 +254,32 @@ test('should load advanced model options from localStorage', async () => { expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ model: 'PurdyPointsModel', riegelExponent: 1.2, + selectedTargetSet: 'B', }); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) + .to.equal('B'); + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) + .to.deep.equal(targetSet2.targets); }); -test('should save advanced model options to localStorage when modified', async () => { +test('should save options to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); - // Update advanced model options + // Update options await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ model: 'CameronModel', riegelExponent: 1.30, + selectedTargetSet: '_race_targets', }); + await wrapper.findComponent({ name: 'target-set-selector' }) + .setValue('B', 'selectedTargetSet'); // Assert data saved to localStorage expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({ model: 'CameronModel', riegelExponent: 1.3, + selectedTargetSet: 'B', })); }); diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -8,7 +8,9 @@ beforeEach(() => { test('should load selected target set from localStorage', async () => { // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-set', '"B"'); + localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ + selectedTargetSet: 'B', + })); // Initialize component const wrapper = shallowMount(SplitCalculator); @@ -65,13 +67,16 @@ test('should load targets from localStorage and pass to splitOutputTable', async test('should correctly handle null target set', async () => { // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-set', '"does_not_exist"'); + localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({ + selectedTargetSet: 'does_not_exist', + })); // Initialize component const wrapper = shallowMount(SplitCalculator); // Assert selection is loaded - expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('does_not_exist'); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) + .to.equal('does_not_exist'); // Assert empty array passed to SplitOutputTable expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([]); @@ -133,8 +138,9 @@ test('should save selected target set to localStorage when modified', async () = .setValue('_race_targets', 'selectedTargetSet'); // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.split-calculator-target-set')) - .to.equal('"_race_targets"'); + expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(JSON.stringify({ + selectedTargetSet: '_race_targets', + })); }); test('should load default units from localStorage and pass to splitOutputTable', async () => { diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js @@ -68,6 +68,7 @@ test('should correctly calculate results according to advanced model options', a await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ model: 'RiegelModel', riegelExponent: 1.10, + selectedTargetSet: '_workout_targets', }); // Calculate result @@ -121,7 +122,19 @@ test('should save input race to localStorage', async () => { })); }); -test('should load selected target set from localStorage', async () => { +test('should save default units setting to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(WorkoutCalculator); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // 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', @@ -167,48 +180,11 @@ test('should load selected target set from localStorage', async () => { }, 'B': targetSet2, })); - localStorage.setItem('running-tools.workout-calculator-target-set', '"B"'); - - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) - .to.equal('B'); - expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) - .to.deep.equal(targetSet2.targets); -}); - -test('should save selected target set to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Select a new target set - await wrapper.findComponent({ name: 'target-set-selector' }) - .setValue('B', 'selectedTargetSet'); - - // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.workout-calculator-target-set')) - .to.equal('"B"'); -}); - -test('should save default units setting to localStorage when modified', async () => { - // Initialize component - const wrapper = shallowMount(WorkoutCalculator); - - // Change default units - await wrapper.find('select[aria-label="Default units"]').setValue('metric'); - await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); - - // New default units should be saved to localStorage - expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); -}); - -test('should load advanced model options from localStorage', async () => { - // Initialize localStorage localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + customTargetNames: true, model: 'PurdyPointsModel', riegelExponent: 1.2, + selectedTargetSet: 'B', })); // Initialize component @@ -216,24 +192,39 @@ test('should load advanced model options from localStorage', async () => { // Assert data loaded expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({ + customTargetNames: true, model: 'PurdyPointsModel', riegelExponent: 1.2, + selectedTargetSet: 'B', }); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) + .to.equal('B'); + expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets) + .to.deep.equal(targetSet2.targets); + expect(wrapper.find('select[aria-label="Target name customization"]').element.value) + .to.equal('true'); }); -test('should save advanced model options to localStorage when modified', async () => { +test('should save options to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(WorkoutCalculator); - // Update advanced model options + // Update options await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({ + customTargetNames: false, model: 'CameronModel', riegelExponent: 1.30, + selectedTargetSet: '_workout_targets', }); + wrapper.find('select[aria-label="Target name customization"]').setValue('true'); + await wrapper.findComponent({ name: 'target-set-selector' }) + .setValue('B', 'selectedTargetSet'); // 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', })); });