commit 6b5d8096614996c42aa3b8605320af8ea73fc667
parent 79d6bee6e188ce7e66ecf9bd6923100e5789fbaf
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 23 Aug 2025 14:49:24 -0700
Merge branch 'reorganize-localstorage' into dev
Diffstat:
25 files changed, 2279 insertions(+), 1327 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"/>
@@ -24,16 +25,17 @@
</select>
</div>
- <div v-if="batchOptions && props.batchInput && props.type === Calculators.Workout"
+ <div v-if="batchOptions && props.type === Calculators.Workout"
v-show="(options as WorkoutOptions).customTargetNames">
Batch column label:
- <input v-model="batchOptions.label" :placeholder="formatDistance(props.batchInput, false)"
+ <input v-model="batchOptions.label" :placeholder="formatDistance(batchOptions.input, false)"
aria-label="Batch column label"/>
</div>
<div v-if="props.type === Calculators.Race || props.type === Calculators.Workout">
Prediction model:
- <select v-model="(options as RaceOptions).model" aria-label="Prediction 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>
<option :value="RacePredictionModels.VO2MaxModel">V̇O₂ Max Model</option>
@@ -43,10 +45,10 @@
</div>
<div v-if="props.type === Calculators.Race || props.type === Calculators.Workout"
- v-show="(options as RaceOptions).model == RacePredictionModels.AverageModel ||
- (options as RaceOptions).model == RacePredictionModels.RiegelModel">
+ v-show="globalOptions.racePredictionOptions.model == RacePredictionModels.AverageModel
+ || globalOptions.racePredictionOptions.model == RacePredictionModels.RiegelModel">
Riegel exponent:
- <decimal-input v-model="(options as RaceOptions).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>
@@ -54,35 +56,29 @@
<script setup lang="ts">
import { Calculators } from '@/core/calculators';
-import type { BatchOptions, StandardOptions, RaceOptions,
+import type { BatchOptions, GlobalOptions, PaceOptions, RaceOptions, SplitOptions,
WorkoutOptions } from '@/core/calculators';
import { RacePredictionModels } from '@/core/racePrediction';
import type { TargetSets } from '@/core/targets';
-import { UnitSystems, formatDistance } from '@/core/units';
-import type { DistanceTime } from '@/core/units';
+import { formatDistance } from '@/core/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
import useObjectModel from '@/composables/useObjectModel';
-type CalculatorOptions = StandardOptions | RaceOptions | WorkoutOptions;
-
-/*
- * The default unit system
- */
-const defaultUnitSystem = defineModel<UnitSystems>('defaultUnitSystem');
+type CalculatorOptions = PaceOptions | RaceOptions | SplitOptions | WorkoutOptions;
const props = defineProps<{
/*
- * The batch calculator input (if applicable, used to generate custom batch label placeholder)
+ * The batch calculator options (if applicable)
*/
- batchInput?: DistanceTime,
+ batchOptions?: BatchOptions,
/*
- * The batch calculator options (if applicable)
+ * The global options
*/
- batchOptions?: BatchOptions,
+ globalOptions: GlobalOptions,
/*
* The calculator options
@@ -100,10 +96,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/components/RaceOptionsInput.vue b/src/components/RaceOptionsInput.vue
@@ -1,39 +0,0 @@
-<template>
- <div>
- Prediction Model:
- <select v-model="model.model" aria-label="Prediction model">
- <option value="AverageModel">Average</option>
- <option value="PurdyPointsModel">Purdy Points Model</option>
- <option value="VO2MaxModel">V̇O₂ Max Model</option>
- <option value="CameronModel">Cameron's Model</option>
- <option value="RiegelModel">Riegel's Model</option>
- </select>
- </div>
- <div>
- Riegel Exponent:
- <decimal-input v-model="model.riegelExponent" aria-label="Riegel exponent" :min="1" :max="1.3"
- :digits="2" :step="0.01"/>
- (default: 1.06)
- </div>
-</template>
-
-<script setup lang="ts">
-import type { RaceOptions } from '@/core/calculators';
-
-import DecimalInput from '@/components/DecimalInput.vue';
-import useObjectModel from '@/composables/useObjectModel';
-
-interface Props {
- /**
- * The component value
- */
- modelValue: RaceOptions,
-}
-
-const props = defineProps<Props>();
-
-// Generate internal ref tied to modelValue prop
-const emit = defineEmits(['update:modelValue']);
-const model = useObjectModel<RaceOptions>(() => props.modelValue,
- (x) => emit('update:modelValue', x));
-</script>
diff --git a/src/core/calculators.ts b/src/core/calculators.ts
@@ -3,10 +3,11 @@
*/
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';
/*
@@ -34,19 +35,24 @@ export interface RaceStats {
/*
* The type for the options specific to each calculator
*/
-export interface StandardOptions {
+export interface GlobalOptions {
+ defaultUnitSystem: UnitSystems,
+ racePredictionOptions: RacePredictionOptions,
+};
+export interface SplitOptions {
selectedTargetSet: string,
-}
-export interface RaceOptions extends StandardOptions {
- model: racePrediction.RacePredictionModels,
- riegelExponent: number,
};
-export interface WorkoutOptions extends RaceOptions {
+export interface PaceOptions extends SplitOptions {
+ input: DistanceTime,
+};
+export type RaceOptions = PaceOptions;
+export interface WorkoutOptions extends PaceOptions {
customTargetNames: boolean,
};
export interface BatchOptions {
calculator: Calculators.Pace | Calculators.Race | Calculators.Workout,
increment: number,
+ input: DistanceTime,
label: string,
rows: number,
};
@@ -73,6 +79,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,
@@ -81,23 +91,24 @@ export const defaultInput: DistanceTime = {
export const defaultBatchOptions: BatchOptions = {
calculator: Calculators.Workout,
increment: 15,
+ input: defaultInput,
label: '',
rows: 20,
};
-export const defaultPaceOptions: StandardOptions = {
+export const defaultPaceOptions: PaceOptions = {
+ input: defaultInput,
selectedTargetSet: '_pace_targets',
};
export const defaultRaceOptions: RaceOptions = {
- model: racePrediction.RacePredictionModels.AverageModel,
- riegelExponent: 1.06,
+ input: defaultInput,
selectedTargetSet: '_race_targets',
};
-export const defaultSplitOptions: StandardOptions = {
+export const defaultSplitOptions: SplitOptions = {
selectedTargetSet: '_split_targets',
};
export const defaultWorkoutOptions: WorkoutOptions = {
customTargetNames: false,
- ...defaultRaceOptions,
+ input: defaultInput,
selectedTargetSet: '_workout_targets',
};
@@ -165,7 +176,7 @@ function calculateStandardResult(input: DistanceTime, target: StandardTarget,
*/
export function calculatePaceResults(input: DistanceTime, target: StandardTarget,
defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
+ preciseDurations: boolean): TargetResult {
return calculateStandardResult(input, target, (d1, t1, d2) => ((t1 / d1) * d2),
(t1, d1, t2) => ((d1 / t1) * t2), defaultUnitSystem, preciseDurations);
@@ -175,18 +186,19 @@ export function calculatePaceResults(input: DistanceTime, target: StandardTarget
* Predict race results from a target
* @param {DistanceTime} input The input race
* @param {StandardTarget} target The race target
- * @param {RaceOptions} options The race prediction options
+ * @param {RacePredictionOptions} racePredictionOptions The race prediction options
* @param {UnitSystems} defaultUnitSystem The default unit system (imperial or metric)
* @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
* @returns {TargetResult} The result
*/
export function calculateRaceResults(input: DistanceTime, target: StandardTarget,
- options: RaceOptions, defaultUnitSystem: UnitSystems,
- preciseDurations: boolean = true): TargetResult {
+ racePredictionOptions: RacePredictionOptions,
+ defaultUnitSystem: UnitSystems, preciseDurations: boolean
+ ): TargetResult {
return calculateStandardResult(input, target,
- (d1, t1, d2) => racePrediction.predictTime(d1, t1, d2, options.model, options.riegelExponent),
- (t1, d1, t2) => racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent),
+ (d1, t1, d2) => racePrediction.predictTime(d1, t1, d2, racePredictionOptions),
+ (t1, d1, t2) => racePrediction.predictDistance(t1, d1, t2, racePredictionOptions),
defaultUnitSystem, preciseDurations);
}
@@ -210,13 +222,15 @@ export function calculateRaceStats(input: DistanceTime): RaceStats {
* Predict workout results from a target
* @param {DistanceTime} input The input race
* @param {WorkoutTarget} target The workout target
- * @param {WorkoutOptions} options The workout options
+ * @param {RacePredictionOptions} racePredictionOptions The race prediction options
+ * @param {Boolean} customTargetNames Whether to use custom target names
* @param {Boolean} preciseDurations Whether to return precise, unrounded, durations
* @returns {TargetResult} The result
*/
export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarget,
- options: WorkoutOptions,
- preciseDurations: boolean = true): TargetResult {
+ racePredictionOptions: RacePredictionOptions,
+ customTargetNames: boolean, preciseDurations: boolean
+ ): TargetResult {
// Initialize distance and time variables
const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
const t1 = input.time;
@@ -229,18 +243,18 @@ export function calculateWorkoutResults(input: DistanceTime, target: WorkoutTarg
d2 = convertDistance(target.distanceValue, target.distanceUnit, DistanceUnits.Meters);
// Get workout split prediction
- t2 = racePrediction.predictTime(d1, input.time, d2, options.model, options.riegelExponent);
+ t2 = racePrediction.predictTime(d1, input.time, d2, racePredictionOptions);
} else {
t2 = target.time;
// Get workout split prediction
- d2 = racePrediction.predictDistance(t1, d1, t2, options.model, options.riegelExponent);
+ d2 = racePrediction.predictDistance(t1, d1, t2, racePredictionOptions);
}
const t3 = (t2 / d2) * d3;
// Return result
return {
- key: (options.customTargetNames && target.customName) || workoutTargetToString(target),
+ key: (customTargetNames && target.customName) || workoutTargetToString(target),
value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true),
pace: '', // Pace not used in workout calculator
result: ResultType.Value,
diff --git a/src/core/migrations.ts b/src/core/migrations.ts
@@ -0,0 +1,182 @@
+/*
+ * Contains a function for migrating localStorage items after app updates
+ */
+
+import { defaultBatchOptions, defaultGlobalOptions, defaultPaceOptions, defaultRaceOptions,
+ defaultSplitOptions, defaultWorkoutOptions } from '@/core/calculators';
+import { deepCopy, getLocalStorage, setLocalStorage, unsetLocalStorage } from '@/core/utils';
+
+/*
+ * The type for string-indexable objects
+ */
+type dict = {
+ [key: string]: json,
+};
+
+/*
+ * The type for JSON-compatable values
+ */
+type json = dict | string | number | boolean;
+
+/**
+ * Get the value of an arbitrary property on an object
+ * @param {dict} obj The object
+ * @param {string} key The property path
+ * @returns {json | undefined} The value of the property
+ */
+function getObjProperty(obj: dict, key: string): json | undefined {
+ const keys = key.split(".");
+ while (true) {
+ if (keys.length === 0) {
+ return obj;
+ } else if (obj[keys[0]] === undefined) {
+ return undefined;
+ } else {
+ obj = obj[keys[0]] as dict;
+ keys.shift();
+ }
+ }
+}
+
+/**
+ * Set the value of an arbitrary property on an object
+ * @param {dict} obj The object
+ * @param {string} key The property path
+ * @param {json} value The new value of the property
+ */
+function setObjProperty(obj: dict, key: string, value: json) {
+ const keys = key.split(".");
+ while (true) {
+ if (keys.length === 1) {
+ obj[keys[0]] = value;
+ return;
+ } else if (obj[keys[0]] === undefined) {
+ obj[keys[0]] = {};
+ obj = obj[keys[0]] as dict;
+ keys.shift();
+ } else {
+ obj = obj[keys[0]] as dict;
+ keys.shift();
+ }
+ }
+}
+
+/**
+ * Remove an arbitrary property on an object
+ * @param {dict} obj The object
+ * @param {string} key The property path
+ */
+function removeObjProperty(obj: dict, key: string) {
+ const keys = key.split(".");
+ while (true) {
+ if (keys.length === 1) {
+ delete obj[keys[0]];
+ return;
+ } else if (obj[keys[0]] === undefined) {
+ return;
+ } else {
+ obj = obj[keys[0]] as dict;
+ keys.shift();
+ }
+ }
+}
+
+/**
+ * Add a property to an existing localStorage item
+ * @param {string} dest The localStorage item
+ * @param {string} key The localStorage item property path
+ * @param {object | string | number | boolean} value The default property value
+ */
+function addProperty(dest: string, key: string, value: object | string | number | boolean) {
+ const dest_value = getLocalStorage<dict>(dest);
+ if (dest_value !== null && getObjProperty(dest_value, key) === undefined) {
+ setObjProperty(dest_value, key, deepCopy(value as json));
+ setLocalStorage(dest, dest_value);
+ }
+}
+
+/**
+ * Move an existing localStorage property to a new location
+ * @param {string} src The original localStorage item
+ * @param {string} src_key The original localStorage item property path
+ * @param {string} dest The new parent localStorage item
+ * @param {string} dest_key The new localStorage item property path
+ * @param {object} dest_default The default value of the new parent localStorage item
+ */
+function moveProperty(src: string, src_key: string, dest: string, dest_key: string,
+ dest_default: object) {
+ const src_value = getLocalStorage<dict>(src);
+ const dest_value = getLocalStorage<dict>(dest) || deepCopy(dest_default as dict);
+ if (src_value !== null && getObjProperty(src_value, src_key) !== undefined) {
+ setObjProperty(dest_value, dest_key, getObjProperty(src_value, src_key) as json);
+ setLocalStorage(dest, dest_value);
+ removeObjProperty(src_value, src_key);
+ setLocalStorage(src, src_value);
+ }
+ addProperty(dest, dest_key, getObjProperty(dest_default as dict, dest_key) as json);
+}
+
+/**
+ * Move an existing localStorage item to a property of another localStorage item
+ * @param {string} src The original localStorage item
+ * @param {string} dest The new parent localStorage item
+ * @param {string} dest_key The new localStorage item property path
+ * @param {object} dest_default The default value of the new parent localStorage item
+ */
+function moveItemToProperty(src: string, dest: string, dest_key: string, dest_default: object) {
+ const src_value = getLocalStorage<dict>(src);
+ const dest_value = getLocalStorage<dict>(dest) || deepCopy(dest_default as dict);
+ if (src_value !== null) {
+ setObjProperty(dest_value, dest_key, src_value);
+ setLocalStorage(dest, dest_value);
+ unsetLocalStorage(src);
+ }
+ addProperty(dest, dest_key, (dest_default as dict)[dest_key]);
+}
+
+/**
+ * Migrate outdated localStorage options
+ */
+export function migrateLocalStorage() {
+ // Move default-unit-system to global-options.defaultUnitSystem (>1.4.1)
+ moveItemToProperty('default-unit-system', 'global-options', 'defaultUnitSystem',
+ defaultGlobalOptions);
+
+ // Move {race,workout}-calculator-options.{model,riegelExponent} into
+ // global-options.racePredictionOptions (>1.4.1)
+ moveProperty('workout-calculator-options', 'model', 'global-options',
+ 'racePredictionOptions.model', defaultGlobalOptions);
+ moveProperty('workout-calculator-options', 'riegelExponent', 'global-options',
+ 'racePredictionOptions.riegelExponent', defaultGlobalOptions);
+ moveProperty('race-calculator-options', 'model', 'global-options',
+ 'racePredictionOptions.model', defaultGlobalOptions);
+ moveProperty('race-calculator-options', 'riegelExponent', 'global-options',
+ 'racePredictionOptions.riegelExponent', defaultGlobalOptions);
+
+ // Add label property to batch-calculator-options (>1.4.1)
+ addProperty('batch-calculator-options', 'label', defaultBatchOptions.label);
+
+ // Add customTargetNames property to workout-calculator-options (>1.4.1)
+ addProperty('workout-calculator-options', 'customTargetNames',
+ defaultWorkoutOptions.customTargetNames);
+
+ // Move *-calculator-input into *-calculator-options (>1.4.1)
+ moveItemToProperty('batch-calculator-input', 'batch-calculator-options',
+ 'input', defaultBatchOptions);
+ moveItemToProperty('pace-calculator-input', 'pace-calculator-options',
+ 'input', defaultPaceOptions);
+ moveItemToProperty('race-calculator-input', 'race-calculator-options',
+ 'input', defaultRaceOptions);
+ moveItemToProperty('workout-calculator-input', 'workout-calculator-options',
+ 'input', defaultWorkoutOptions);
+
+ // Move *-calculator-target-set into *-calculator-options (>1.4.1)
+ moveItemToProperty('pace-calculator-target-set', 'pace-calculator-options',
+ 'selectedTargetSet', defaultPaceOptions);
+ moveItemToProperty('race-calculator-target-set', 'race-calculator-options',
+ 'selectedTargetSet', defaultRaceOptions);
+ moveItemToProperty('split-calculator-target-set', 'split-calculator-options',
+ 'selectedTargetSet', defaultSplitOptions);
+ moveItemToProperty('workout-calculator-target-set', 'workout-calculator-options',
+ 'selectedTargetSet', defaultWorkoutOptions);
+}
diff --git a/src/core/racePrediction.ts b/src/core/racePrediction.ts
@@ -14,6 +14,22 @@ export enum RacePredictionModels {
};
/*
+ * The type for race prediction options
+ */
+export interface RacePredictionOptions {
+ model: RacePredictionModels,
+ riegelExponent: number,
+};
+
+/*
+ * 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 {
@@ -438,22 +454,21 @@ const AverageModel = {
* @param {number} d1 The distance of the input race in meters
* @param {number} t1 The finish time of the input race in seconds
* @param {number} d2 The distance of the output race in meters
- * @param {string} model The race prediction model to use
- * @param {number} c The value of the exponent in Pete Riegel's Model
+ * @param {RacePredictionOptions} options The race prediction options
+ * @param {number} The predicted finish time in seconds
*/
export function predictTime(d1: number, t1: number, d2: number,
- model: RacePredictionModels = RacePredictionModels.AverageModel,
- c: number = 1.06): number {
- switch (model) {
+ options: RacePredictionOptions): number {
+ switch (options.model) {
default:
case RacePredictionModels.AverageModel:
- return AverageModel.predictTime(d1, t1, d2, c);
+ return AverageModel.predictTime(d1, t1, d2, options.riegelExponent);
case RacePredictionModels.PurdyPointsModel:
return PurdyPointsModel.predictTime(d1, t1, d2);
case RacePredictionModels.VO2MaxModel:
return VO2MaxModel.predictTime(d1, t1, d2);
case RacePredictionModels.RiegelModel:
- return RiegelModel.predictTime(d1, t1, d2, c);
+ return RiegelModel.predictTime(d1, t1, d2, options.riegelExponent);
case RacePredictionModels.CameronModel:
return CameronModel.predictTime(d1, t1, d2);
}
@@ -464,22 +479,21 @@ export function predictTime(d1: number, t1: number, d2: number,
* @param {number} t1 The finish time of the input race in seconds
* @param {number} d1 The distance of the input race in meters
* @param {number} t2 The finish time of the output race in seconds
- * @param {string} model The race prediction model to use
- * @param {number} c The value of the exponent in Pete Riegel's Model
+ * @param {RacePredictionOptions} options The race prediction options
+ * @param {number} The predicted finish distance in meters
*/
export function predictDistance(t1: number, d1: number, t2: number,
- model: RacePredictionModels = RacePredictionModels.AverageModel,
- c: number = 1.06) {
- switch (model) {
+ options: RacePredictionOptions): number {
+ switch (options.model) {
default:
case RacePredictionModels.AverageModel:
- return AverageModel.predictDistance(t1, d1, t2, c);
+ return AverageModel.predictDistance(t1, d1, t2, options.riegelExponent);
case RacePredictionModels.PurdyPointsModel:
return PurdyPointsModel.predictDistance(t1, d1, t2);
case RacePredictionModels.VO2MaxModel:
return VO2MaxModel.predictDistance(t1, d1, t2);
case RacePredictionModels.RiegelModel:
- return RiegelModel.predictDistance(t1, d1, t2, c);
+ return RiegelModel.predictDistance(t1, d1, t2, options.riegelExponent);
case RacePredictionModels.CameronModel:
return CameronModel.predictDistance(t1, d1, t2);
}
diff --git a/src/core/utils.ts b/src/core/utils.ts
@@ -2,8 +2,6 @@
* Contains utility functions for handling nested objects and interacting with localStorage
*/
-import { defaultRaceOptions, defaultWorkoutOptions } from '@/core/calculators';
-
// The global localStorage prefix
const LocalStoragePrefix = 'running-tools';
@@ -55,69 +53,3 @@ export function setLocalStorage<Type>(key: string, value: Type) {
export function unsetLocalStorage(key: string) {
localStorage.removeItem(`${LocalStoragePrefix}.${key}`);
}
-
-/**
- * Migrate outdated localStorage options
- */
-export function migrateLocalStorage() {
- /* eslint-disable @typescript-eslint/no-explicit-any */
-
- // Add label property to batch-calculator-options (>1.4.1)
- const batchOptions = getLocalStorage<any>('batch-calculator-options');
- if (batchOptions !== null && batchOptions.label === undefined) {
- batchOptions.label = '';
- setLocalStorage('batch-calculator-options', batchOptions);
- }
-
- // Move pace-calculator-target-set into new pace-calculator-options (>1.4.1)
- const paceSelectedTargetSet = getLocalStorage<string>('pace-calculator-target-set');
- if (paceSelectedTargetSet !== null) {
- const paceOptions = { selectedTargetSet: paceSelectedTargetSet };
- setLocalStorage('pace-calculator-options', paceOptions);
- unsetLocalStorage('pace-calculator-target-set');
- }
-
- // Move race-calculator-target-set into race-calculator-options (>1.4.1)
- const raceSelectedTargetSet = getLocalStorage<string>('race-calculator-target-set');
- const raceOptions = getLocalStorage<any>('race-calculator-options')
- || deepCopy(defaultRaceOptions);
- if (raceSelectedTargetSet !== null) {
- raceOptions.selectedTargetSet = raceSelectedTargetSet;
- setLocalStorage('race-calculator-options', raceOptions);
- unsetLocalStorage('race-calculator-target-set');
- }
- if (raceOptions !== null && raceOptions.selectedTargetSet === undefined) {
- raceOptions.selectedTargetSet = defaultRaceOptions.selectedTargetSet;
- setLocalStorage('race-calculator-options', raceOptions);
- }
-
- // Move split-calculator-target-set into new split-calculator-options (>1.4.1)
- const splitSelectedTargetSet = getLocalStorage<string>('split-calculator-target-set');
- if (splitSelectedTargetSet !== null) {
- const splitOptions = { selectedTargetSet: splitSelectedTargetSet };
- setLocalStorage('split-calculator-options', splitOptions);
- unsetLocalStorage('split-calculator-target-set');
- }
-
- // Move workout-calculator-target-set into workout-calculator-options (>1.4.1)
- const workoutSelectedTargetSet = getLocalStorage<string>('workout-calculator-target-set');
- const workoutOptions = getLocalStorage<any>('workout-calculator-options')
- || deepCopy(defaultWorkoutOptions);
- if (workoutSelectedTargetSet !== null) {
- workoutOptions.selectedTargetSet = workoutSelectedTargetSet;
- setLocalStorage('workout-calculator-options', workoutOptions);
- unsetLocalStorage('workout-calculator-target-set');
- }
- if (workoutOptions !== null && workoutOptions.selectedTargetSet === undefined) {
- workoutOptions.selectedTargetSet = defaultWorkoutOptions.selectedTargetSet;
- setLocalStorage('workout-calculator-options', workoutOptions);
- }
-
- // Add customTargetNames property to workout-calculator-options (>1.4.1)
- if (workoutOptions.customTargetNames === undefined) {
- workoutOptions.customTargetNames = false;
- setLocalStorage('workout-calculator-options', workoutOptions);
- }
-
- /* eslint-enable @typescript-eslint/no-explicit-any */
-}
diff --git a/src/main.ts b/src/main.ts
@@ -2,7 +2,7 @@ import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router';
-import { migrateLocalStorage } from '@/core/utils';
+import { migrateLocalStorage } from '@/core/migrations';
import '@/assets/global.css';
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -2,20 +2,20 @@
<div class="calculator">
<h2>Batch Input</h2>
<div class="input">
- <pace-input v-model="input" aria-label="Input"/>
+ <pace-input v-model="batchOptions.input" aria-label="Input"/>
</div>
<h2>Batch Options</h2>
<div class="input">
<div>
Increment:
- <time-input v-model="options.increment" label="Duration increment" :show-hours="false"/>
+ <time-input v-model="batchOptions.increment" label="Duration increment" :show-hours="false"/>
×
- <integer-input v-model="options.rows" min="1" aria-label="Number of rows"/>
+ <integer-input v-model="batchOptions.rows" min="1" aria-label="Number of rows"/>
</div>
<div>
Calculator:
- <select aria-label="Calculator" v-model="options.calculator">
+ <select aria-label="Calculator" v-model="batchOptions.calculator">
<option :value="calculators.Calculators.Pace">Pace Calculator</option>
<option :value="calculators.Calculators.Race">Race Calculator</option>
<option :value="calculators.Calculators.Workout">Workout Calculator</option>
@@ -27,9 +27,9 @@
<summary>
<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:targetSets="targetSets" :type="options.calculator"/>
+ <advanced-options-input v-model:batch-options="batchOptions"
+ v-model:globalOptions="globalOptions" v-model:options="calcOptions"
+ v-model:targetSets="targetSets" :type="batchOptions.calculator"/>
</details>
<h2>Batch Results</h2>
@@ -44,10 +44,10 @@
import { computed } from 'vue';
import * as calculators from '@/core/calculators';
-import type { BatchOptions, RaceOptions, StandardOptions, TargetResult,
+import type { BatchOptions, GlobalOptions, PaceOptions, RaceOptions, 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';
@@ -59,20 +59,25 @@ import TimeInput from '@/components/TimeInput.vue';
import useStorage from '@/composables/useStorage';
/*
- * The input pace
+ * The global options
*/
-const input = useStorage<DistanceTime>('batch-calculator-input', calculators.defaultInput);
+const globalOptions = useStorage<GlobalOptions>('global-options', calculators.defaultGlobalOptions);
/*
- * The batch input options
+ * The batch calculator options
*/
-const options = useStorage<BatchOptions>('batch-calculator-options',
+const batchOptions = useStorage<BatchOptions>('batch-calculator-options',
calculators.defaultBatchOptions);
/*
- * The default unit system
+ * The options for each calculator
*/
-const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
+const paceOptions = useStorage<PaceOptions>('pace-calculator-options',
+ calculators.defaultPaceOptions);
+const raceOptions = useStorage<RaceOptions>('race-calculator-options',
+ calculators.defaultRaceOptions);
+const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options',
+ calculators.defaultWorkoutOptions);
/*
* The target sets for each calculator
@@ -85,21 +90,11 @@ const workoutTargetSets = useStorage<targetUtils.WorkoutTargetSets>(
'workout-calculator-target-sets', targetUtils.defaultWorkoutTargetSets);
/*
- * The options for each calculator
- */
-const paceOptions = useStorage<StandardOptions>('pace-calculator-options',
- calculators.defaultPaceOptions);
-const raceOptions = useStorage<RaceOptions>('race-calculator-options',
- calculators.defaultRaceOptions);
-const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options',
- calculators.defaultWorkoutOptions);
-
-/*
* The input distance
*/
const inputDistance = computed<Distance>(() => ({
- distanceValue: input.value.distanceValue,
- distanceUnit: input.value.distanceUnit,
+ distanceValue: batchOptions.value.input.distanceValue,
+ distanceUnit: batchOptions.value.input.distanceUnit,
}));
/*
@@ -107,8 +102,8 @@ const inputDistance = computed<Distance>(() => ({
*/
const inputTimes = computed<Array<number>>(() => {
const results = [];
- for (let i = 0; i < options.value.rows; i++) {
- results.push(input.value.time + options.value.increment * i);
+ for (let i = 0; i < batchOptions.value.rows; i++) {
+ results.push(batchOptions.value.input.time + batchOptions.value.increment * i);
}
return results;
});
@@ -118,7 +113,7 @@ const inputTimes = computed<Array<number>>(() => {
*/
const targetSets = computed<targetUtils.TargetSets>({
get: () => {
- switch (options.value.calculator) {
+ switch (batchOptions.value.calculator) {
case (calculators.Calculators.Pace): {
return paceTargetSets.value;
}
@@ -132,7 +127,7 @@ const targetSets = computed<targetUtils.TargetSets>({
}
},
set: (newValue: targetUtils.TargetSets) => {
- switch (options.value.calculator) {
+ switch (batchOptions.value.calculator) {
case (calculators.Calculators.Pace): {
paceTargetSets.value = newValue as targetUtils.StandardTargetSets;
break;
@@ -153,9 +148,9 @@ const targetSets = computed<targetUtils.TargetSets>({
/*
* The options for the current calculator
*/
-const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({
+const calcOptions = computed<PaceOptions | RaceOptions | WorkoutOptions>({
get: () => {
- switch (options.value.calculator) {
+ switch (batchOptions.value.calculator) {
case (calculators.Calculators.Pace): {
return paceOptions.value;
}
@@ -168,10 +163,10 @@ const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({
}
}
},
- set: (newValue: StandardOptions | RaceOptions | WorkoutOptions) => {
- switch(options.value.calculator) {
+ set: (newValue: PaceOptions | RaceOptions | WorkoutOptions) => {
+ switch(batchOptions.value.calculator) {
case (calculators.Calculators.Pace): {
- paceOptions.value = newValue as StandardOptions;
+ paceOptions.value = newValue as PaceOptions;
break;
}
case (calculators.Calculators.Race): {
@@ -191,18 +186,19 @@ const calcOptions = computed<StandardOptions | RaceOptions | WorkoutOptions>({
* The appropriate calculate_results function for the current calculator
*/
const calculateResult = computed<(x: DistanceTime, y: targetUtils.Target) => TargetResult>(() => {
- switch(options.value.calculator) {
+ switch(batchOptions.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,
- 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, false);
+ globalOptions.value.racePredictionOptions, workoutOptions.value.customTargetNames, false);
}
}
});
@@ -211,11 +207,11 @@ const calculateResult = computed<(x: DistanceTime, y: targetUtils.Target) => Tar
* The label to render for the batch column
*/
const batchColumnLabel = computed<string>(() => {
- if (options.value.calculator == calculators.Calculators.Workout &&
- (calcOptions.value as WorkoutOptions).customTargetNames && options.value.label) {
- return options.value.label;
+ if (batchOptions.value.calculator == calculators.Calculators.Workout &&
+ (calcOptions.value as WorkoutOptions).customTargetNames && batchOptions.value.label) {
+ return batchOptions.value.label;
} else {
- return formatDistance(input.value, false);
+ return formatDistance(batchOptions.value.input, false);
}
});
</script>
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -2,33 +2,31 @@
<div class="calculator">
<h2>Input Pace</h2>
<div class="input">
- <pace-input v-model="input"/>
+ <pace-input v-model="paceOptions.input"/>
</div>
<details>
<summary>
<h2>Advanced Options</h2>
</summary>
- <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem"
- v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Pace"/>
+ <advanced-options-input v-model:globalOptions="globalOptions"
+ v-model:options="paceOptions" 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)"
- :targets="targetSets[options.selectedTargetSet] ?
- targetSets[options.selectedTargetSet].targets : []"/>
+ calculatePaceResults(paceOptions.input, x, globalOptions.defaultUnitSystem, true)"
+ :targets="targetSets[paceOptions.selectedTargetSet] ?
+ targetSets[paceOptions.selectedTargetSet].targets : []"/>
</div>
</template>
<script setup lang="ts">
-import { Calculators, calculatePaceResults, defaultInput,
+import { Calculators, calculatePaceResults, defaultGlobalOptions,
defaultPaceOptions } from '@/core/calculators';
-import type { StandardOptions } from '@/core/calculators';
+import type { GlobalOptions, PaceOptions } 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';
import PaceInput from '@/components/PaceInput.vue';
@@ -37,19 +35,14 @@ import SingleOutputTable from '@/components/SingleOutputTable.vue';
import useStorage from '@/composables/useStorage';
/*
- * The input pace
+ * The global options
*/
-const input = useStorage<DistanceTime>('pace-calculator-input', defaultInput);
+const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions);
/*
- * The default unit system
+ * The pace calculator options
*/
-const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
-
-/*
- * The current selected target set
- */
-const options = useStorage<StandardOptions>('pace-calculator-options', defaultPaceOptions);
+const paceOptions = useStorage<PaceOptions>('pace-calculator-options', defaultPaceOptions);
/*
* The target sets
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -2,7 +2,7 @@
<div class="calculator">
<h2>Input Race Result</h2>
<div class="input">
- <pace-input v-model="input" label="Input race"/>
+ <pace-input v-model="raceOptions.input" label="Input race"/>
</div>
<details>
@@ -26,28 +26,28 @@
<summary>
<h2>Advanced Options</h2>
</summary>
- <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem"
- v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Race"/>
+ <advanced-options-input v-model:globalOptions="globalOptions"
+ v-model:options="raceOptions" 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, defaultUnitSystem, true)"
- :targets="targetSets[options.selectedTargetSet] ?
- targetSets[options.selectedTargetSet].targets : []"/>
+ <single-output-table class="output" show-pace :calculate-result="x =>
+ calculateRaceResults(raceOptions.input, x, globalOptions.racePredictionOptions,
+ globalOptions.defaultUnitSystem, true)"
+ :targets="targetSets[raceOptions.selectedTargetSet] ?
+ targetSets[raceOptions.selectedTargetSet].targets : []"/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
-import { Calculators, calculateRaceResults, calculateRaceStats, defaultInput,
+import { Calculators, calculateRaceResults, calculateRaceStats, defaultGlobalOptions,
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 type { DistanceTime } from '@/core/units';
+import { formatNumber } from '@/core/units';
import AdvancedOptionsInput from '@/components/AdvancedOptionsInput.vue';
import PaceInput from '@/components/PaceInput.vue';
@@ -56,22 +56,17 @@ import SingleOutputTable from '@/components/SingleOutputTable.vue';
import useStorage from '@/composables/useStorage';
/*
- * The input race
+ * The global options
*/
-const input = useStorage<DistanceTime>('race-calculator-input', defaultInput);
-
-/*
- * The default unit system
- */
-const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
+const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions);
/*
* The race calculator options
*/
-const options = useStorage<RaceOptions>('race-calculator-options', defaultRaceOptions);
+const raceOptions = useStorage<RaceOptions>('race-calculator-options', defaultRaceOptions);
/*
- * The target sets
+ * The race calculator target sets
*/
const targetSets = useStorage<StandardTargetSets>('race-calculator-target-sets',
defaultRaceTargetSets);
@@ -79,7 +74,7 @@ const targetSets = useStorage<StandardTargetSets>('race-calculator-target-sets',
/*
* The statistics for the current input race
*/
-const raceStats = computed<RaceStats>(() => calculateRaceStats(input.value));
+const raceStats = computed<RaceStats>(() => calculateRaceStats(raceOptions.value.input));
</script>
<style scoped>
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"
- v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Split"/>
+ <advanced-options-input v-model:globalOptions="globalOptions"
+ v-model:options="splitOptions" 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, SplitOptions } 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,17 +26,17 @@ 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
*/
-const options = useStorage<StandardOptions>('split-calculator-options', defaultSplitOptions);
+const splitOptions = useStorage<SplitOptions>('split-calculator-options', defaultSplitOptions);
/*
- * The default output targets
+ * The split calculator target sets
*/
const targetSets = useStorage<SplitTargetSets>('split-calculator-target-sets',
defaultSplitTargetSets);
@@ -46,15 +46,15 @@ const targetSets = useStorage<SplitTargetSets>('split-calculator-target-sets',
*/
const targetSet = computed({
get: () => {
- if (targetSets.value[options.value.selectedTargetSet]) {
- return targetSets.value[options.value.selectedTargetSet].targets
+ if (targetSets.value[splitOptions.value.selectedTargetSet]) {
+ return targetSets.value[splitOptions.value.selectedTargetSet].targets
} else {
return []
}
},
set: (newValue) => {
- if (targetSets.value[options.value.selectedTargetSet]) {
- targetSets.value[options.value.selectedTargetSet].targets = newValue;
+ if (targetSets.value[splitOptions.value.selectedTargetSet]) {
+ targetSets.value[splitOptions.value.selectedTargetSet].targets = newValue;
}
},
});
diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue
@@ -2,33 +2,32 @@
<div class="calculator">
<h2>Input Race Result</h2>
<div class="input">
- <pace-input v-model="input" label="Input race"/>
+ <pace-input v-model="workoutOptions.input" label="Input race"/>
</div>
<details>
<summary>
<h2>Advanced Options</h2>
</summary>
- <advanced-options-input v-model:defaultUnitSystem="defaultUnitSystem"
- v-model:options="options" v-model:targetSets="targetSets" :type="Calculators.Workout"/>
+ <advanced-options-input v-model:globalOptions="globalOptions"
+ v-model:options="workoutOptions" 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, true)"
- :targets="targetSets[options.selectedTargetSet] ?
- targetSets[options.selectedTargetSet].targets : []"/>
+ <single-output-table class="output" :calculate-result="x =>
+ calculateWorkoutResults(workoutOptions.input, x as WorkoutTarget,
+ globalOptions.racePredictionOptions, workoutOptions.customTargetNames, true)"
+ :targets="targetSets[workoutOptions.selectedTargetSet] ?
+ targetSets[workoutOptions.selectedTargetSet].targets : []"/>
</div>
</template>
<script setup lang="ts">
-import { Calculators, calculateWorkoutResults, defaultInput,
+import { Calculators, calculateWorkoutResults, defaultGlobalOptions,
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';
import PaceInput from '@/components/PaceInput.vue';
@@ -37,19 +36,14 @@ import SingleOutputTable from '@/components/SingleOutputTable.vue';
import useStorage from '@/composables/useStorage';
/*
- * The input race
+ * The global options
*/
-const input = useStorage<DistanceTime>('workout-calculator-input', defaultInput);
-
-/*
- * The default unit system
- */
-const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
+const globalOptions = useStorage<GlobalOptions>('global-options', defaultGlobalOptions);
/*
* The race prediction options
*/
-const options = useStorage<WorkoutOptions>('workout-calculator-options', defaultWorkoutOptions);
+const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options', defaultWorkoutOptions);
/*
* The target sets
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,43 +212,51 @@ 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
- expect(await page.evaluate(() => localStorage.length)).toEqual(16);
- expect(await page.evaluate(() => localStorage.getItem('running-tools.default-unit-system')))
- .toEqual(JSON.stringify('metric'));
+ // Assert global localStorage entries are correct
+ expect(await page.evaluate(() => localStorage.length)).toEqual(12);
+ 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(() =>
- localStorage.getItem('running-tools.batch-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- }));
- expect(await page.evaluate(() =>
localStorage.getItem('running-tools.batch-calculator-options'))).toEqual(JSON.stringify({
calculator: 'race',
increment: 10,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ },
label: '',
rows: 15,
}));
// Assert localStorage entries for the pace calculator are correct
- expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.pace-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 930,
- }));
const paceCalculatorKey = parseInt(JSON.parse(await page.evaluate(() =>
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-options'))).toEqual(JSON.stringify({
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 930,
+ },
+ selectedTargetSet: paceCalculatorKey.toString(),
+ }));
+ expect(await page.evaluate(() =>
localStorage.getItem('running-tools.pace-calculator-target-sets'))).toEqual(JSON.stringify({
_pace_targets: {
name: 'Common Pace Targets',
@@ -297,15 +305,12 @@ test('Cross-calculator', async ({ page }) => {
// Assert localStorage entries for the race calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.race-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- }));
- expect(await page.evaluate(() =>
localStorage.getItem('running-tools.race-calculator-options'))).toEqual(JSON.stringify({
- model: 'RiegelModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ },
selectedTargetSet: '_race_targets',
}));
expect(await page.evaluate(() =>
@@ -375,16 +380,13 @@ test('Cross-calculator', async ({ page }) => {
// Assert localStorage entries for the workout calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.workout-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 1,
- distanceUnit: 'miles',
- time: 301,
- }));
- expect(await page.evaluate(() =>
localStorage.getItem('running-tools.workout-calculator-options'))).toEqual(JSON.stringify({
customTargetNames: true,
- model: 'VO2MaxModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 301,
+ },
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);
}
});
@@ -724,34 +726,44 @@ test('v1.4.1 Migration', async ({ page }) => {
{
// Reload the app and assert general localStorage entries are correct
await page.goto('/');
- 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.length)).toEqual(12);
+ expect(await page.evaluate(() =>
+ localStorage.getItem('running-tools.default-unit-system'))).toBeNull();
+ expect(await page.evaluate(() =>
+ localStorage.getItem('running-tools.global-options'))).toEqual(JSON.stringify({
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.06,
+ },
+ }));
// Assert localStorage entries for the batch calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.batch-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- }));
+ localStorage.getItem('running-tools.batch-calculator-input'))).toBeNull();
expect(await page.evaluate(() =>
localStorage.getItem('running-tools.batch-calculator-options'))).toEqual(JSON.stringify({
calculator: 'race',
increment: 10,
rows: 15,
label: '',
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ },
}));
// Assert localStorage entries for the pace calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.pace-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 930,
- }));
+ localStorage.getItem('running-tools.pace-calculator-input'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('running-tools.pace-calculator-options')))
.toEqual(JSON.stringify({
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 930,
+ },
selectedTargetSet: '123456789',
}));
expect(await page.evaluate(() =>
@@ -805,15 +817,14 @@ test('v1.4.1 Migration', async ({ page }) => {
// Assert localStorage entries for the race calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.race-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 630,
- }));
+ localStorage.getItem('running-tools.race-calculator-input'))).toBeNull();
expect(await page.evaluate(() => localStorage.getItem('running-tools.race-calculator-options')))
.toEqual(JSON.stringify({
- model: 'RiegelModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ },
selectedTargetSet: '_race_targets',
}));
expect(await page.evaluate(() =>
@@ -849,6 +860,8 @@ test('v1.4.1 Migration', async ({ page }) => {
selectedTargetSet: '_split_targets',
}));
expect(await page.evaluate(() =>
+ localStorage.getItem('running-tools.split-calculator-target-set'))).toBeNull();
+ expect(await page.evaluate(() =>
localStorage.getItem('running-tools.split-calculator-target-sets'))).toEqual(JSON.stringify({
_split_targets: {
name: '5K 1600m Splits',
@@ -859,8 +872,6 @@ test('v1.4.1 Migration', async ({ page }) => {
],
},
}));
- expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.split-calculator-target-set'))).toBeNull();
// Assert localStorage entries for the unit calculator are correct
expect(await page.evaluate(() => localStorage.getItem('running-tools.unit-calculator-category')))
@@ -886,17 +897,16 @@ test('v1.4.1 Migration', async ({ page }) => {
// Assert localStorage entries for the workout calculator are correct
expect(await page.evaluate(() =>
- localStorage.getItem('running-tools.workout-calculator-input'))).toEqual(JSON.stringify({
- distanceValue: 1,
- distanceUnit: 'miles',
- time: 301,
- }));
+ localStorage.getItem('running-tools.workout-calculator-input'))).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,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 301,
+ },
+ selectedTargetSet: '_workout_targets',
}));
expect(await page.evaluate(() =>
localStorage.getItem('running-tools.workout-calculator-target-set'))).toBeNull();
@@ -929,8 +939,26 @@ test('v1.4.1 Migration', async ({ page }) => {
// Assert UI options are up to date
// Very similar to the previous "go back and assert the options are not resset" section
{
- // Assert pace results are correct (inputs and options not reset)
+ // Assert batch options are correct
await page.getByRole('button', { name: 'Batch Calculator' }).click();
+ await expect(page.getByLabel('Input distance value')).toHaveValue('2.00');
+ await expect(page.getByLabel('Input distance unit')).toHaveValue('miles');
+ await expect(page.getByLabel('Input duration hours')).toHaveValue('0');
+ await expect(page.getByLabel('Input duration minutes')).toHaveValue('10');
+ await expect(page.getByLabel('Input duration seconds')).toHaveValue('30.00');
+ await expect(page.getByLabel('Duration increment minutes')).toHaveValue('00');
+ await expect(page.getByLabel('Duration increment seconds')).toHaveValue('10.00');
+ await expect(page.getByLabel('Number of rows')).toHaveValue('15');
+ await expect(page.getByLabel('Calculator')).toHaveValue('race');
+
+ // Assert advanced options are correct for race calculator mode
+ await page.getByText('Advanced Options').click();
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('_race_targets');
+ await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
+ await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
+
+ // 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);
@@ -942,8 +970,12 @@ test('v1.4.1 Migration', async ({ page }) => {
await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17);
await expect(page.getByRole('row')).toHaveCount(16);
- // Assert pace results are correct (inputs and options not reset, new pace targets loaded)
+ // Assert advanced options are correct for pace calculator mode
await page.getByLabel('Calculator').selectOption('Pace Calculator');
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('123456789');
+
+ // Assert pace results are correct (inputs and options not reset, new pace targets loaded)
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(4);
@@ -955,14 +987,21 @@ test('v1.4.1 Migration', async ({ page }) => {
await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4);
await expect(page.getByRole('row')).toHaveCount(16);
- // Assert workout results are correct (new workout options loaded)
+ // Assert advanced options are correct for workout calculator mode
await page.getByLabel('Calculator').selectOption('Workout Calculator');
+ await page.getByText('Advanced Options').click();
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('_workout_targets');
await expect(page.getByLabel('Target name customization')).toHaveValue('false');
+ await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
+ await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
+
+ // Assert workout results are correct (new workout options loaded)
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 @ 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:41');
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');
@@ -976,6 +1015,16 @@ test('v1.4.1 Migration', async ({ page }) => {
await page.getByRole('link', { name: 'Back' }).click();
await page.getByRole('button', { name: 'Pace Calculator' }).click();
+ // Assert pace calculator options are correct
+ await expect(page.getByLabel('Input distance value')).toHaveValue('2.00');
+ await expect(page.getByLabel('Input distance unit')).toHaveValue('miles');
+ await expect(page.getByLabel('Input duration hours')).toHaveValue('0');
+ await expect(page.getByLabel('Input duration minutes')).toHaveValue('15');
+ await expect(page.getByLabel('Input duration seconds')).toHaveValue('30.00');
+ await page.getByText('Advanced Options').click();
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('123456789');
+
// Assert paces are correct (input pace not reset)
await expect(page.getByRole('row').nth(1)).toHaveText('0.4 km' + '1:55.58');
await expect(page.getByRole('row').nth(2)).toHaveText('800 m' + '3:51.15');
@@ -986,6 +1035,18 @@ test('v1.4.1 Migration', async ({ page }) => {
await page.getByRole('link', { name: 'Back' }).click();
await page.getByRole('button', { name: 'Race Calculator' }).click();
+ // Assert race calculator options are correct
+ await expect(page.getByLabel('Input race distance value')).toHaveValue('2.00');
+ await expect(page.getByLabel('Input race distance unit')).toHaveValue('miles');
+ await expect(page.getByLabel('Input race duration hours')).toHaveValue('0');
+ await expect(page.getByLabel('Input race duration minutes')).toHaveValue('10');
+ await expect(page.getByLabel('Input race duration seconds')).toHaveValue('30.00');
+ await page.getByText('Advanced Options').click();
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('_race_targets');
+ await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
+ await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
+
// 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');
@@ -995,6 +1056,10 @@ test('v1.4.1 Migration', async ({ page }) => {
await page.getByRole('link', { name: 'Back' }).click();
await page.getByRole('button', { name: 'Split Calculator' }).click();
+ // Assert split calculator options are correct
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('_split_targets');
+
// Assert times and paces are correct (split times not reset)
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:23 / km');
@@ -1015,9 +1080,22 @@ test('v1.4.1 Migration', async ({ page }) => {
await page.getByRole('link', { name: 'Back' }).click();
await page.getByRole('button', { name: 'Workout Calculator' }).click();
+ // Assert workout calculator options are correct
+ await expect(page.getByLabel('Input race distance value')).toHaveValue('1.00');
+ await expect(page.getByLabel('Input race distance unit')).toHaveValue('miles');
+ await expect(page.getByLabel('Input race duration hours')).toHaveValue('0');
+ await expect(page.getByLabel('Input race duration minutes')).toHaveValue('05');
+ await expect(page.getByLabel('Input race duration seconds')).toHaveValue('01.00');
+ await page.getByText('Advanced Options').click();
+ await expect(page.getByLabel('Default units')).toHaveValue('metric');
+ await expect(page.getByLabel('Selected target set')).toHaveValue('_workout_targets');
+ await expect(page.getByLabel('Target name customization')).toHaveValue('false');
+ await expect(page.getByLabel('Prediction model')).toHaveValue('RiegelModel');
+ await expect(page.getByLabel('Riegel Exponent')).toHaveValue('1.06');
+
// 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' + '5:44.38');
await expect(page.getByRole('row')).toHaveCount(5);
}
});
diff --git a/tests/unit/components/AdvancedOptionsInput.spec.js b/tests/unit/components/AdvancedOptionsInput.spec.js
@@ -6,8 +6,19 @@ 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: {
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: 'B',
},
targetSets: {
@@ -66,10 +77,19 @@ test('should be correctly render race options according to props', () => {
// Initialize component
const wrapper = shallowMount(AdvancedOptionsInput, {
propsData: {
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ },
options: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
type: 'race',
@@ -96,10 +116,19 @@ test('should render riegel exponent field only for supported race prediction mod
// Initialize component
const wrapper = shallowMount(AdvancedOptionsInput, {
propsData: {
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.2,
+ },
+ },
options: {
- model: 'AverageModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
type: 'race',
@@ -132,7 +161,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',
},
@@ -158,11 +193,20 @@ test('should be correctly render workout options according to props', () => {
// Initialize component
const wrapper = shallowMount(AdvancedOptionsInput, {
propsData: {
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ },
options: {
customTargetNames: true,
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -187,11 +231,20 @@ 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,
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -204,24 +257,33 @@ test('should only show batch column label field when applicable', async () => {
expect(wrapper.findAll('input[aria-label="Batch column label"]')).to.have
.length(0);
- // Add batchInput and batchOptions but disable workout target name customization
+ // Add batchOptions but disable workout target name customization
await wrapper.setProps({
- batchInput: { // added
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- },
batchOptions: { // added
calculator: 'workout',
increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: 'foo',
rows: 15,
},
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.30,
+ },
+ },
options: {
customTargetNames: false, // disabled
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -233,22 +295,31 @@ test('should only show batch column label field when applicable', async () => {
// Enable workout target name customization
await wrapper.setProps({
- batchInput: {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- },
batchOptions: {
calculator: 'workout',
increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: 'foo',
rows: 15,
},
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.30,
+ },
+ },
options: {
customTargetNames: true, // enabled
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -262,21 +333,30 @@ test('should only show batch column label field when applicable', async () => {
// Switch to race calculator
await wrapper.setProps({
- batchInput: {
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- },
batchOptions: {
calculator: 'workout',
increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: 'foo',
rows: 15,
},
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'CameronModel',
+ riegelExponent: 1.30,
+ },
+ },
options: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -292,11 +372,20 @@ 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,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: 'B',
},
targetSets: {
@@ -356,11 +445,20 @@ test('should emit input events when options are modified', async () => {
// Initialize component
const wrapper = shallowMount(AdvancedOptionsInput, {
propsData: {
- defaultUnitSystem: 'metric',
+ globalOptions: {
+ defaultUnitSystem: 'metric',
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ },
options: {
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_new',
},
targetSets: {},
@@ -394,7 +492,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',
@@ -415,28 +535,22 @@ test('should emit input events when options are modified', async () => {
}]]);
expect(wrapper.emitted()['update:options']).to.deep.equal([
[{
- customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
- selectedTargetSet: 'B',
- }],
- [{
- customTargetNames: true,
- model: 'AverageModel',
- riegelExponent: 1.06,
- selectedTargetSet: 'B',
- }],
- [{
- customTargetNames: true,
- model: 'CameronModel',
- riegelExponent: 1.06,
- selectedTargetSet: 'B',
+ customTargetNames: false,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: 'B',
}],
[{
- customTargetNames: true,
- model: 'CameronModel',
- riegelExponent: 1.3,
- selectedTargetSet: 'B',
+ customTargetNames: true,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: 'B',
}],
]);
});
diff --git a/tests/unit/components/RaceOptionsInput.spec.js b/tests/unit/components/RaceOptionsInput.spec.js
@@ -1,53 +0,0 @@
-import { test, expect } from 'vitest';
-import { shallowMount } from '@vue/test-utils';
-import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
-
-test('should be initialized to modelValue', () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptionsInput, {
- propsData: {
- modelValue: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
- }
- },
- });
-
- // Assert input fields are correct
- expect(wrapper.find('select').element.value).to.equal('PurdyPointsModel');
- expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
-});
-
-test('should emit event when inputs are modified', async () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptionsInput, {
- propsData: {
- modelValue: {
- model: 'AverageModel',
- riegelExponent: 1.06,
- },
- },
- });
-
- // Update model
- await wrapper.find('select').setValue('CameronModel');
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- ]);
-
- // Update Riegel exponent
- await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- [{
- model: 'CameronModel',
- riegelExponent: 1.3,
- }],
- ]);
-});
diff --git a/tests/unit/core/calculators.spec.js b/tests/unit/core/calculators.spec.js
@@ -14,7 +14,7 @@ describe('calculatePaceResults method', () => {
type: 'distance',
};
- const result = calculators.calculatePaceResults(input, target, 'metric');
+ const result = calculators.calculatePaceResults(input, target, 'metric', true);
expect(result).to.deep.equal({
key: '20 m',
@@ -36,8 +36,8 @@ describe('calculatePaceResults method', () => {
type: 'time',
};
- const result1 = calculators.calculatePaceResults(input, target, 'metric');
- const result2 = calculators.calculatePaceResults(input, target, 'imperial');
+ const result1 = calculators.calculatePaceResults(input, target, 'metric', true);
+ const result2 = calculators.calculatePaceResults(input, target, 'imperial', true);
expect(result1.key).to.equal('1.61 km');
expect(result1.value).to.equal('10:00');
@@ -65,12 +65,13 @@ describe('calculateRaceResults method', () => {
distanceUnit: 'kilometers',
type: 'distance',
};
- const options = {
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
}
- const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result.key).to.equal('10 km');
expect(result.value).to.equal('41:34.80');
@@ -89,13 +90,15 @@ describe('calculateRaceResults method', () => {
time: 2495,
type: 'time',
};
- const options = {
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
- }
+ };
- const result1 = calculators.calculateRaceResults(input, target, options, 'metric');
- const result2 = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result1 = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'metric', true);
+ const result2 = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result1.key).to.equal('10.00 km');
expect(result1.value).to.equal('41:35');
@@ -121,12 +124,13 @@ describe('calculateRaceResults method', () => {
distanceUnit: 'kilometers',
type: 'distance',
};
- const options = {
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
}
- const result = calculators.calculateRaceResults(input, target, options, 'imperial');
+ const result = calculators.calculateRaceResults(input, target, racePredictionOptions,
+ 'imperial', true);
expect(result.key).to.equal('5 km');
expect(result.value).to.equal('17:11.78');
@@ -167,13 +171,13 @@ describe('calculateWorkoutResults method', () => {
splitUnit: 'meters',
type: 'distance',
};
- const options = {
- customTargetNames: false,
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
}
- const result = calculators.calculateWorkoutResults(input, target, options);
+ const result = calculators.calculateWorkoutResults(input, target, racePredictionOptions,
+ false, true);
expect(result.key).to.equal('1000 m @ 5 km');
expect(result.value).to.equal('3:26.36');
@@ -204,21 +208,19 @@ describe('calculateWorkoutResults method', () => {
type: 'distance',
customName: 'my custom name',
};
- const options_a = {
- customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.12,
- };
- const options_b = {
- customTargetNames: true,
+ const racePredictionOptions = {
model: 'RiegelModel',
riegelExponent: 1.12,
};
- const result1a = calculators.calculateWorkoutResults(input, target_1, options_a);
- const result1b = calculators.calculateWorkoutResults(input, target_1, options_b);
- const result2a = calculators.calculateWorkoutResults(input, target_2, options_a);
- const result2b = calculators.calculateWorkoutResults(input, target_2, options_b);
+ const result1a = calculators.calculateWorkoutResults(input, target_1, racePredictionOptions,
+ false, true);
+ const result1b = calculators.calculateWorkoutResults(input, target_1, racePredictionOptions,
+ true, true);
+ const result2a = calculators.calculateWorkoutResults(input, target_2, racePredictionOptions,
+ false, true);
+ const result2b = calculators.calculateWorkoutResults(input, target_2, racePredictionOptions,
+ true, true);
expect(result1a.key).to.equal('1000 m @ 5 km');
expect(result1b.key).to.equal('1000 m @ 5 km');
@@ -238,13 +240,13 @@ describe('calculateWorkoutResults method', () => {
splitUnit: 'miles',
type: 'time',
};
- const options = {
- customTargetNames: false,
+ const racePredictionOptions = {
model: 'AverageModel',
riegelExponent: 1.06,
}
- const result = calculators.calculateWorkoutResults(input, target, options);
+ const result = calculators.calculateWorkoutResults(input, target, racePredictionOptions, false,
+ true);
expect(result.key).to.equal('1 mi @ 41:35');
expect(result.value).to.equal('6:41.50');
diff --git a/tests/unit/core/migration.spec.js b/tests/unit/core/migration.spec.js
@@ -0,0 +1,150 @@
+import { beforeEach, describe, test, expect } from 'vitest';
+import { migrateLocalStorage } from '@/core/migrations';
+import { detectDefaultUnitSystem } from '@/core/units';
+
+beforeEach(() => {
+ localStorage.clear();
+});
+
+describe('migrateLocalStorage method', () => {
+ test('should correctly migrate <=1.4.1 calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.default-unit-system', '"imperial"');
+
+ localStorage.setItem('running-tools.batch-calculator-input',
+ '{"distanceValue":100,"distanceUnit":"meters","time":10}');
+ localStorage.setItem('running-tools.batch-calculator-options',
+ '{"calculator":"race","increment":32,"rows":15}');
+
+ localStorage.setItem('running-tools.pace-calculator-input',
+ '{"distanceValue":110,"distanceUnit":"meters","time":11}');
+ localStorage.setItem('running-tools.pace-calculator-target-set', '"A"');
+
+ localStorage.setItem('running-tools.race-calculator-input',
+ '{"distanceValue":120,"distanceUnit":"meters","time":12}');
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ localStorage.setItem('running-tools.race-calculator-target-set', '"B"');
+
+ localStorage.setItem('running-tools.split-calculator-target-set', '"C"');
+
+ localStorage.setItem('running-tools.workout-calculator-input',
+ '{"distanceValue":130,"distanceUnit":"meters","time":13}');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.08}');
+ localStorage.setItem('running-tools.workout-calculator-target-set', '"D"');
+
+ // Run migrations
+ migrateLocalStorage();
+
+ // Assert localStorage entries correctly migrated
+ expect(localStorage.getItem('running-tools.default-unit-system')).to.equal(null);
+ expect(localStorage.getItem('running-tools.global-options')).to.equal(
+ '{"defaultUnitSystem":"imperial","racePredictionOptions":{"model":"RiegelModel",' +
+ '"riegelExponent":1.07}}');
+
+ expect(localStorage.getItem('running-tools.batch-calculator-input')).to.equal(null);
+ expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(
+ '{"calculator":"race","increment":32,"rows":15,"label":"",' +
+ '"input":{"distanceValue":100,"distanceUnit":"meters","time":10}}');
+
+ expect(localStorage.getItem('running-tools.pace-calculator-input')).to.equal(null);
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
+ '{"input":{"distanceValue":110,"distanceUnit":"meters","time":11},"selectedTargetSet":"A"}');
+ expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal(null);
+
+ expect(localStorage.getItem('running-tools.race-calculator-input')).to.equal(null);
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"input":{"distanceValue":120,"distanceUnit":"meters","time":12},"selectedTargetSet":"B"}');
+ expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal(null);
+
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
+ '{"selectedTargetSet":"C"}');
+ expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal(null);
+
+ expect(localStorage.getItem('running-tools.workout-calculator-input')).to.equal(null);
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"customTargetNames":false,"input":{"distanceValue":130,"distanceUnit":"meters",' +
+ '"time":13},"selectedTargetSet":"D"}');
+ expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal(null);
+ });
+
+ test('should correctly migrate partial <=1.4.1 calculator options using default values', async () => {
+ // Initialize localStorage
+ // default-unit-system, *-calculator-input, and *-calculator-target-set left undefined
+ localStorage.setItem('running-tools.batch-calculator-options',
+ '{"calculator":"race","increment":32,"rows":15}');
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.08}');
+
+ // Run migrations
+ migrateLocalStorage();
+
+ // Assert localStorage entries correctly migrated
+ const defaultUnitSystem = detectDefaultUnitSystem();
+ expect(localStorage.getItem('running-tools.global-options')).to.equal(
+ `{"defaultUnitSystem":"${defaultUnitSystem}",` +
+ '"racePredictionOptions":{"model":"RiegelModel","riegelExponent":1.07}}');
+ expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(
+ '{"calculator":"race","increment":32,"rows":15,"label":"",' +
+ '"input":{"distanceValue":5,"distanceUnit":"kilometers","time":1200}}');
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"input":{"distanceValue":5,"distanceUnit":"kilometers","time":1200},' +
+ '"selectedTargetSet":"_race_targets"}');
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"customTargetNames":false,"input":{"distanceValue":5,"distanceUnit":"kilometers",' +
+ '"time":1200},"selectedTargetSet":"_workout_targets"}');
+ });
+
+ test('should not modify >1.4.1 calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.global-calculator-options',
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ localStorage.setItem('running-tools.batch-calculator-options',
+ '{"calculator":"race","increment":32,"input":{"distanceValue":100,"distanceUnit":"meters",' +
+ '"time":10},"label":"foo","rows":15}');
+ localStorage.setItem('running-tools.pace-calculator-options',
+ '{"input":{"distanceValue":110,"distanceUnit":"meters","time":11},"selectedTargetSet":"A"}');
+ localStorage.setItem('running-tools.race-calculator-options',
+ '{"input":{"distanceValue":120,"distanceUnit":"meters","time":12},"selectedTargetSet":"B"}');
+ localStorage.setItem('running-tools.split-calculator-options',
+ '{"selectedTargetSet":"C"}');
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"customTargetNames":true,"input":{"distanceValue":120,"distanceUnit":"meters","time":12},' +
+ '"selectedTargetSet":"D"}');
+
+ // Run migrations
+ migrateLocalStorage();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.global-calculator-options')).to.equal(
+ '{"model":"RiegelModel","riegelExponent":1.07}');
+ expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(
+ '{"calculator":"race","increment":32,"input":{"distanceValue":100,"distanceUnit":"meters",' +
+ '"time":10},"label":"foo","rows":15}');
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
+ '{"input":{"distanceValue":110,"distanceUnit":"meters","time":11},"selectedTargetSet":"A"}');
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
+ '{"input":{"distanceValue":120,"distanceUnit":"meters","time":12},"selectedTargetSet":"B"}');
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
+ '{"selectedTargetSet":"C"}');
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"customTargetNames":true,"input":{"distanceValue":120,"distanceUnit":"meters","time":12},' +
+ '"selectedTargetSet":"D"}');
+ });
+
+ test('should not modify missing calculator options', async () => {
+ // Run migrations
+ migrateLocalStorage();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.global-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(null);
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(null);
+ });
+});
diff --git a/tests/unit/core/racePrediction.spec.js b/tests/unit/core/racePrediction.spec.js
@@ -4,66 +4,108 @@ import * as racePrediction from '@/core/racePrediction';
describe('predictTime method', () => {
describe('PredictTime method', () => {
test('Average Model', () => {
- const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ riegelExponent: 1.06,
+ });
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
- const result = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.equal(expected);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'AverageModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'PurdyPointsModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2490, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'PurdyPointsModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'VO2MaxModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2488, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'VO2MaxModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'VO2MaxModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Cameron Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'CameronModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(2500, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'CameronModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictTime(5000, 1200, 10000, 'RiegelModel');
+ const result = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(2502, 1);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictTime(5000, 1200, 5000, 'RiegelModel');
+ const result = racePrediction.predictTime(5000, 1200, 5000, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(1200, 0.001);
});
});
@@ -72,66 +114,108 @@ describe('predictTime method', () => {
describe('predictDistance method', () => {
describe('Average Model', () => {
test('Predictions should be correct', () => {
- const riegel = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const cameron = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
- const vo2Max = racePrediction.predictTime(5000, 1200, 10000, 'AverageModel');
+ const riegel = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'RiegelModel',
+ riegel: 1.06,
+ });
+ const cameron = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
+ const purdyPoints = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
+ const vo2Max = racePrediction.predictTime(5000, 1200, 10000, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
const expected = (riegel + cameron + purdyPoints + vo2Max) / 4;
- const result = racePrediction.predictDistance(1200, 5000, expected);
+ const result = racePrediction.predictDistance(1200, 5000, expected, {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'AverageModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'AverageModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Purdy Points Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2490, 'PurdyPointsModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2490, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'PurdyPointsModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'PurdyPointsModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('VO2 Max Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2488, 'VO2MaxModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2488, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'VO2MaxModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'VO2MaxModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Cameron Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2500, 'CameronModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2500, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'CameronModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'CameronModel',
+ riegel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
describe('Riegel Model', () => {
test('Predictions should be approximately correct', () => {
- const result = racePrediction.predictDistance(1200, 5000, 2502, 'RiegelModel');
+ const result = racePrediction.predictDistance(1200, 5000, 2502, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(10000, 10);
});
test('Should predict identical times for itentical distances', () => {
- const result = racePrediction.predictDistance(1200, 5000, 1200, 'RiegelModel');
+ const result = racePrediction.predictDistance(1200, 5000, 1200, {
+ model: 'RiegelModel',
+ RiegelModel: 1.06,
+ });
expect(result).to.be.closeTo(5000, 0.001);
});
});
diff --git a/tests/unit/core/utils.spec.js b/tests/unit/core/utils.spec.js
@@ -130,7 +130,7 @@ describe('deepEqual method', () => {
});
});
-describe('get method', () => {
+describe('getLocalStorage method', () => {
test('should correctly parse correct localStorage item', async () => {
// Initialize localStorage
localStorage.setItem('running-tools.foo', '{"bar":123}');
@@ -156,7 +156,7 @@ describe('get method', () => {
});
});
-describe('set method', () => {
+describe('setLocalStorage method', () => {
test('should correctly set new localStorage item', async () => {
// Set localStorage item
utils.setLocalStorage('foo', { baz: 456 });
@@ -177,95 +177,30 @@ describe('set method', () => {
});
});
-describe('migrate method', () => {
- test('should correctly migrate <=1.4.1 calculator options', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.batch-calculator-options',
- '{"calculator":"race","increment":32,"rows":15}');
- localStorage.setItem('running-tools.pace-calculator-target-set', '"A"');
- localStorage.setItem('running-tools.race-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.07}');
- localStorage.setItem('running-tools.race-calculator-target-set', '"B"');
- localStorage.setItem('running-tools.split-calculator-target-set', '"C"');
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.08}');
- localStorage.setItem('running-tools.workout-calculator-target-set', '"D"');
-
- // Run migrations
- utils.migrateLocalStorage();
-
- // Assert localStorage entries correctly migrated
- expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(
- '{"calculator":"race","increment":32,"rows":15,"label":""}');
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
- '{"selectedTargetSet":"A"}');
- expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
- '{"selectedTargetSet":"C"}');
- expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal(null);
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"D",' +
- '"customTargetNames":false}');
- expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal(null);
- });
-
- test('should correctly migrate partial <=1.4.1 calculator options', async () => {
- // Initialize localStorage (workout-target-set option missing)
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.08}');
+describe('unsetLocalStorage method', () => {
+ test('should correctly remove existing localStorage item', async () => {
+ // Set localStorage item
+ localStorage.setItem('running-tools.foo', '1');
+ localStorage.setItem('running-tools.bar', '2');
- // Run migrations
- utils.migrateLocalStorage();
+ // Remove localStorage item
+ utils.unsetLocalStorage('bar');
- // Assert localStorage entries correctly migrated
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.08,"selectedTargetSet":"_workout_targets",' +
- '"customTargetNames":false}');
+ // Assert localStorage updated correctly
+ expect(localStorage.getItem('running-tools.foo')).to.equal('1');
+ expect(localStorage.getItem('running-tools.bar')).to.equal(null);
+ expect(localStorage.length).to.equal(1);
});
- test('should not modify >1.4.1 calculator options', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.batch-calculator-options',
- '{"calculator":"race","increment":32,"label":"foo","rows":15}');
- localStorage.setItem('running-tools.pace-calculator-options',
- '{"selectedTargetSet":"A"}');
- localStorage.setItem('running-tools.race-calculator-options',
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- localStorage.setItem('running-tools.split-calculator-options',
- '{"selectedTargetSet":"C"}');
- localStorage.setItem('running-tools.workout-calculator-options',
- '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
- '"selectedTargetSet":"D"}');
-
- // Run migrations
- utils.migrateLocalStorage();
-
- // Assert localStorage entries not modified
- expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(
- '{"calculator":"race","increment":32,"label":"foo","rows":15}');
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(
- '{"selectedTargetSet":"A"}');
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(
- '{"model":"RiegelModel","riegelExponent":1.07,"selectedTargetSet":"B"}');
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(
- '{"selectedTargetSet":"C"}');
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
- '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.08,' +
- '"selectedTargetSet":"D"}');
- });
+ test('should remove non-existant localStorage item without error', async () => {
+ // Set localStorage item
+ localStorage.setItem('running-tools.foo', '1');
- test('should not modify missing calculator options', async () => {
- // Run migrations
- utils.migrateLocalStorage();
+ // Remove localStorage item
+ utils.unsetLocalStorage('missing');
- // Assert localStorage entries not modified
- expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.split-calculator-options')).to.equal(null);
- expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(null);
+ // Assert localStorage updated correctly
+ expect(localStorage.length).to.equal(1);
+ expect(localStorage.getItem('running-tools.foo')).to.equal('1');
});
});
diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js
@@ -1,59 +1,108 @@
import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import BatchCalculator from '@/views/BatchCalculator.vue';
+import { defaultTargetSets } from '@/core/targets';
+import { detectDefaultUnitSystem } from '@/core/units';
beforeEach(() => {
localStorage.clear();
-})
-
-test('should load input from localStorage', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.batch-calculator-input', JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- }));
+});
+test('should initialize regular options to default values', async () => {
// Initialize component
const wrapper = shallowMount(BatchCalculator);
- // Assert options loaded
+ // Assert regular options are initialized
expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
});
- expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.batchInput).to.deep.equal({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
+ expect(wrapper.findComponent({ name: 'time-input' }).vm.modelValue).to.equal(15);
+ expect(wrapper.findComponent({ name: 'integer-input' }).vm.modelValue).to.equal(20);
+ expect(wrapper.find('select[aria-label="Calculator"]').element.value).to.equal('workout');
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
+ defaultUnitSystem: detectDefaultUnitSystem(),
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.batchOptions).to.deep.equal({
+ calculator: 'workout',
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ increment: 15,
+ label: '',
+ rows: 20,
});
});
-test('should save input to localStorage when modified', async () => {
+test('should initialize calculator options to default values', async () => {
// Initialize component
const wrapper = shallowMount(BatchCalculator);
- // Update input pace
- await wrapper.findComponent({ name: 'pace-input' }).setValue({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
+ // Assert pace calculator options are initialized
+ await wrapper.find('select[aria-label="Calculator"]').setValue('pace');
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_pace_targets',
});
+ expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._pace_targets.targets);
- // Assert input saved
- expect(localStorage.getItem('running-tools.batch-calculator-input')).to.equal(JSON.stringify({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- }));
+ // 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({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_race_targets',
+ });
+ expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._race_targets.targets);
+
+ // Assert workout calculator options are loaded
+ await wrapper.find('select[aria-label="Calculator"]').setValue('workout');
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ customTargetNames: false,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_workout_targets',
+ });
+ expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._workout_targets.targets);
});
-test('should load batch options from localStorage', async () => {
+test('should load regular options from localStorage', async () => {
// Initialize localStorage
+ localStorage.setItem('running-tools.global-options', JSON.stringify({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ }));
localStorage.setItem('running-tools.batch-calculator-options', JSON.stringify({
calculator: 'race',
increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: 'foo',
rows: 15,
}));
@@ -61,99 +110,37 @@ test('should load batch options from localStorage', async () => {
// 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,
+ },
+ });
+
// Assert options loaded
expect(wrapper.find('select[aria-label="Calculator"]').element.value).to.equal('race');
+ expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ });
expect(wrapper.findComponent({ name: 'time-input' }).vm.modelValue).to.equal(32);
expect(wrapper.findComponent({ name: 'integer-input' }).vm.modelValue).to.equal(15);
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.batchOptions).to.deep.equal({
calculator: 'race',
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
increment: 32,
label: 'foo',
rows: 15,
});
});
-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 = [
@@ -207,17 +194,28 @@ test('should load calculator options from localStorage', async () => {
}
}));
localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 300,
+ },
selectedTargetSet: 'A',
}));
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: 'C',
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: 'E',
}));
@@ -227,6 +225,11 @@ test('should load calculator options from localStorage', async () => {
// Assert pace calculator options are loaded
await wrapper.find('select[aria-label="Calculator"]').setValue('pace');
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 1.0,
+ distanceUnit: 'miles',
+ time: 300,
+ },
selectedTargetSet: 'A',
});
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -235,8 +238,11 @@ 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({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: 'C',
});
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -246,14 +252,137 @@ 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,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: 'E',
});
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
.to.deep.equal(selectedTargetSets[2].targets);
});
+test('should save regular 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,
+ },
+ }));
+
+ // Update input pace
+ await wrapper.findComponent({ name: 'pace-input' }).setValue({
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ });
+
+ // Assert options saved
+ expect(localStorage.getItem('running-tools.batch-calculator-options')).to.equal(JSON.stringify({
+ calculator: 'workout',
+ increment: 15,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ label: '',
+ rows: 20,
+ }));
+
+ // 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,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ label: '',
+ rows: 15,
+ }));
+
+ // Update batch column label
+ await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ calculator: 'workout',
+ increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ label: 'foo',
+ rows: 15,
+ }));
+});
+
test('should save calculator options to localStorage when modified', async () => {
// Initialize localStorage
const selectedTargetSets = [
@@ -289,6 +418,11 @@ test('should save calculator options to localStorage when modified', async () =>
}
}));
localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 300,
+ },
selectedTargetSet: 'B',
}));
localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify({
@@ -301,8 +435,11 @@ test('should save calculator options to localStorage when modified', async () =>
}
}));
localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: 'D',
}));
localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({
@@ -316,8 +453,11 @@ test('should save calculator options to localStorage when modified', async () =>
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
customWorkoutNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: 'F',
}));
@@ -327,6 +467,11 @@ test('should save calculator options to localStorage when modified', async () =>
// Update pace calculator options X
await wrapper.find('select[aria-label="Calculator"]').setValue('pace');
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ input: {
+ distanceValue: 1.0,
+ distanceUnit: 'miles',
+ time: 300,
+ },
selectedTargetSet: 'A',
}, 'options');
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -335,8 +480,11 @@ 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({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: 'C',
}, 'options');
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -346,8 +494,11 @@ 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,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: 'E',
}, 'options');
expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets)
@@ -355,17 +506,28 @@ test('should save calculator options to localStorage when modified', async () =>
// Assert options saved to localStorage
expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(JSON.stringify({
+ input: {
+ distanceValue: 1.0,
+ distanceUnit: 'miles',
+ time: 300,
+ },
selectedTargetSet: 'A',
}));
expect(localStorage.getItem('running-tools.race-calculator-options')).to.equal(JSON.stringify({
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: 'C',
}));
expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
customTargetNames: true,
- model: 'RiegelModel',
- riegelExponent: 1.1,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: 'E',
}));
});
@@ -434,6 +596,11 @@ test('should pass correct input props to DoubleOutputTable', async () => {
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
calculator: 'workout',
increment: 10,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: 'foo',
rows: 15,
}, 'batchOptions');
@@ -451,8 +618,11 @@ test('should pass correct input props to DoubleOutputTable', async () => {
// Enable target name customization
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: true,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_workout_targets',
}, 'options');
@@ -494,14 +664,14 @@ test('should correctly set AdvancedOptionsInput props', async () => {
await wrapper.findComponent({ name: 'integer-input' }).setValue(15);
// Assert batch props are correct
- expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.batchInput).to.deep.equal({
- distanceValue: 2,
- distanceUnit: 'miles',
- time: 600,
- });
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.batchOptions).to.deep.equal({
calculator: 'workout',
increment: 32,
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 600,
+ },
label: '',
rows: 15,
});
@@ -509,43 +679,88 @@ 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({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_pace_targets',
});
// 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.globalOptions).to.deep.equal({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ });
expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
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.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,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_workout_targets',
});
});
test('should correctly calculate outputs', async () => {
// Initialize localStorage
+ 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({
+ input: {
+ distanceValue: 1.1,
+ distanceUnit: 'miles',
+ time: 310,
+ },
selectedTargetSet: '_race_targets',
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
}));
localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
+ customTargetNames: false,
+ input: {
+ distanceValue: 1.2,
+ distanceUnit: 'miles',
+ time: 320,
+ },
selectedTargetSet: '_workout_targets',
- model: 'RiegelModel',
- riegelExponent: 1.1,
}));
- localStorage.setItem('running-tools.default-unit-system', '"imperial"');
// Initialize component
const wrapper = shallowMount(BatchCalculator);
@@ -579,8 +794,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
@@ -2,10 +2,183 @@ import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import PaceCalculator from '@/views/PaceCalculator.vue';
import { defaultTargetSets } from '@/core/targets';
+import { detectDefaultUnitSystem } from '@/core/units';
beforeEach(() => {
localStorage.clear();
-})
+});
+
+test('should initialize options to default values', async () => {
+ // Initialize component
+ const wrapper = shallowMount(PaceCalculator);
+
+ // Assert options are initialized
+ expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions
+ .defaultUnitSystem).to.equal(detectDefaultUnitSystem());
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_pace_targets',
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
+ .to.deep.equal({ _pace_targets: defaultTargetSets._pace_targets });
+ expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._pace_targets.targets);
+});
+
+test('should load options from localStorage', 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 localStorage
+ localStorage.setItem('running-tools.global-options', JSON.stringify({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ }));
+ localStorage.setItem('running-tools.pace-calculator-target-sets', JSON.stringify(targetSets));
+ localStorage.setItem('running-tools.pace-calculator-options', JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
+
+ // Initialize component
+ const wrapper = shallowMount(PaceCalculator);
+
+ // Assert options are loaded
+ expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions
+ .defaultUnitSystem).to.equal('imperial');
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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 options 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);
+
+ // 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,
+ },
+ }));
+
+ // Update input pace
+ await wrapper.findComponent({ name: 'pace-input' }).setValue({
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ });
+
+ // New input pace should be saved to localStorage
+ expect(localStorage.getItem('running-tools.pace-calculator-options')).to.equal(JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: '_pace_targets',
+ }));
+
+ // 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({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
+});
test('should correctly calculate time results', async () => {
// Initialize component
@@ -36,7 +209,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 +221,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 +237,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 });
@@ -80,15 +263,27 @@ test('should correctly handle null target set', async () => {
const wrapper = shallowMount(PaceCalculator);
// Switch to invalid target set
- await wrapper.findComponent({ name: 'advanced-options-input' })
- .setValue({ selectedTargetSet: 'does_not_exist' }, 'options');
+ await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ 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({ selectedTargetSet: '_pace_targets' }, 'options');
+ await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_pace_targets'
+ }, 'options');
// Assert valid targets passed to SingleOutputTable component
const paceTargets = defaultTargetSets._pace_targets.targets;
@@ -96,103 +291,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 () => {
- // 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',
- }));
-
+test('should correctly set AdvancedOptionsInput type prop', async () => {
// 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
@@ -2,39 +2,195 @@ import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import RaceCalculator from '@/views/RaceCalculator.vue';
import { defaultTargetSets } from '@/core/targets';
+import { detectDefaultUnitSystem } from '@/core/units';
beforeEach(() => {
localStorage.clear();
-})
+});
-test('should correctly predict race times', async () => {
+test('should initialize options to default values', async () => {
// Initialize component
const wrapper = shallowMount(RaceCalculator);
- // Enter input race data
- await wrapper.findComponent({ name: 'pace-input' }).setValue({
+ // Assert options are initialized
+ expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
distanceValue: 5,
distanceUnit: 'kilometers',
time: 1200,
});
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
+ defaultUnitSystem: detectDefaultUnitSystem(),
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_race_targets',
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
+ .to.deep.equal({ _race_targets: defaultTargetSets._race_targets });
+ expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._race_targets.targets);
+});
- // Calculate result
- const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
- const result = calculateResult({
- distanceValue: 10,
- distanceUnit: 'kilometers',
- type: 'distance',
+test('should load options from localStorage', 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 localStorage
+ localStorage.setItem('running-tools.global-options', JSON.stringify({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ }));
+ localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify(targetSets));
+ localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
+
+ // Initialize component
+ const wrapper = shallowMount(RaceCalculator);
+
+ // Assert options are loaded
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
});
+ expect(wrapper.findComponent({ name: 'pace-input' }).vm.modelValue).to.deep.equal({
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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);
+});
- // Assert result is correct
- expect(result.key).to.equal('10 km');
- expect(result.value).to.equal('41:34.80');
- expect(result.pace).to.equal('6:41 / mi');
- expect(result.result).to.equal('value');
- expect(result.sort).to.be.closeTo(2494.80, 0.01);
+test('should save options 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 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,
+ },
+ }));
+
+ // Update input race
+ 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-options')).to.equal(JSON.stringify({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: '_race_targets',
+ }));
+
+ // 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({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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({
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
});
-test('should correctly calculate distance results according to default units setting', async () => {
+test('should correctly predict race times', async () => {
// Initialize component
const wrapper = shallowMount(RaceCalculator);
@@ -45,32 +201,20 @@ test('should correctly calculate distance results according to default units set
time: 1200,
});
- // Set default units
- await wrapper.findComponent({ name: 'advanced-options-input' })
- .setValue('metric', 'defaultUnitSystem');
-
- // Get calculate result function
+ // Calculate result
const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
+ const result = calculateResult({
+ distanceValue: 10,
+ distanceUnit: 'kilometers',
+ type: 'distance',
+ });
// 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.key).to.equal('10 km');
+ expect(result.value).to.equal('41:34.80');
expect(result.pace).to.equal('6:41 / mi');
- expect(result.result).to.equal('key');
- expect(result.sort).to.equal(2495);
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(2494.80, 0.01);
});
test('should show paces in results table', async () => {
@@ -87,8 +231,11 @@ test('should correctly handle null target set', async () => {
// Switch to invalid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: 'does_not_exist',
}, 'options');
@@ -97,8 +244,11 @@ test('should correctly handle null target set', async () => {
// Switch to valid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_race_targets',
}, 'options');
@@ -131,7 +281,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);
@@ -142,16 +292,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({
- model: 'RiegelModel', // changed from the Riegel Model
- riegelExponent: 1.06,
- selectedTargetSet: '_race_targets',
- }, 'options');
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'RiegelModel', // changed from the Average Model
+ riegelExponent: 1.06,
+ },
+ }, 'globalOptions');
// Calculate result
- const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
- let result = calculateResult({
+ result = calculateResult({
distanceValue: 10,
distanceUnit: 'kilometers',
type: 'distance',
@@ -162,10 +350,12 @@ test('should correctly calculate results according to model options', async () =
// Update Riegel Exponent
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- model: 'RiegelModel',
- riegelExponent: 1, // changed from 1.06
- selectedTargetSet: '_race_targets',
- }, 'options');
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1, // changed from 1.06
+ },
+ }, 'globalOptions');
// Calculate result
result = calculateResult({
@@ -178,113 +368,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,
- }));
-
- // 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({
- 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({
- 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 () => {
+test('should correctly set AdvancedOptionsInput type prop', async () => {
// Initialize component
const wrapper = shallowMount(RaceCalculator);
- // Update options
- await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- 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({
- 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
@@ -1,13 +1,60 @@
import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import SplitCalculator from '@/views/SplitCalculator.vue';
+import { defaultTargetSets } from '@/core/targets';
+import { detectDefaultUnitSystem } from '@/core/units';
beforeEach(() => {
localStorage.clear();
-})
+});
+
+test('should initialize options to default values', async () => {
+ // Initialize component
+ const wrapper = shallowMount(SplitCalculator);
+
+ // Assert options are initialized
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions
+ .defaultUnitSystem).to.equal(detectDefaultUnitSystem());
+ expect(wrapper.findComponent({ name: 'split-output-table' }).vm.defaultUnitSystem)
+ .to.equal(detectDefaultUnitSystem());
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ selectedTargetSet: '_split_targets',
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
+ .to.deep.equal({ _split_targets: defaultTargetSets._split_targets });
+ expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue)
+ .to.deep.equal(defaultTargetSets._split_targets.targets);
+});
+
+test('should load options from localStorage', async () => {
+ const targetSets = {
+ '_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: [
+ { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers', split: 180 },
+ { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers', split: 190 },
+ { type: 'distance', distanceValue: 3000, distanceUnit: 'meters', split: 200 },
+ ],
+ },
+ };
-test('should load selected target set from localStorage', async () => {
// Initialize localStorage
+ localStorage.setItem('running-tools.global-options', JSON.stringify({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ }));
+ localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify(targetSets));
localStorage.setItem('running-tools.split-calculator-options', JSON.stringify({
selectedTargetSet: 'B',
}));
@@ -15,15 +62,22 @@ test('should load selected target set from localStorage', async () => {
// Initialize component
const wrapper = shallowMount(SplitCalculator);
- // Assert selection is loaded
+ // Assert options are 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');
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: 'split-output-table' }).vm.modelValue)
+ .to.deep.equal(targetSets.B.targets);
});
-test('should load targets from localStorage and pass to splitOutputTable', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({
+test('should save options to localStorage when modified', async () => {
+ const targetSets1 = {
'_split_targets': {
name: 'Split targets',
targets: [
@@ -40,32 +94,89 @@ test('should load targets from localStorage and pass to splitOutputTable', async
{ 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 },
+ ],
+ },
+ };
// Initialize component
const wrapper = shallowMount(SplitCalculator);
- // Assert default split targets are initially 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' },
- ]);
+ // Set default units setting
+ await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ defaultUnitSystem: 'metric',
+ 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: 'metric',
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ }));
- // Select a new target set
+ // Update default units setting
+ 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,
+ },
+ }));
+
+ // 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: '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 },
- ]);
+ // 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: 'B',
+ }));
+
+ // Update target sets via SplitOutputTable
+ await wrapper.findComponent({ name: 'split-output-table' }).setValue(targetSets2.B.targets);
+
+ // 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 correctly handle null target set', async () => {
@@ -98,100 +209,6 @@ test('should correctly handle null target set', async () => {
]);
});
-test('should update targets in localStorage when modified by splitOutputTable', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({
- '_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 },
- ],
- },
- }));
-
- // 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',
- 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 },
- ],
- },
- }));
-});
-
-test('should save selected target set to localStorage when modified', async () => {
- // Initialize component
- const wrapper = shallowMount(SplitCalculator);
-
- // Select a new target set
- await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- selectedTargetSet: '_race_targets',
- }, 'options');
-
- // New selected target set should be saved to localStorage
- 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 () => {
- // 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');
-
- // 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');
-});
-
-test('should save default units setting to localStorage when modified', async () => {
- // Initialize component
- const wrapper = shallowMount(SplitCalculator);
-
- // Set default units setting
- await wrapper.findComponent({ name: 'advanced-options-input' })
- .setValue('metric', 'defaultUnitSystem');
-
- // New default units should be saved to localStorage
- expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"metric"');
-
- // Set 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 correctly set AdvancedOptionsInput type prop', async () => {
// Initialize component
const wrapper = shallowMount(SplitCalculator);
diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js
@@ -2,10 +2,261 @@ import { beforeEach, test, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import WorkoutCalculator from '@/views/WorkoutCalculator.vue';
import { defaultTargetSets } from '@/core/targets';
+import { detectDefaultUnitSystem } from '@/core/units';
beforeEach(() => {
localStorage.clear();
-})
+});
+
+test('should initialize options to default values', async () => {
+ // Initialize component
+ const wrapper = shallowMount(WorkoutCalculator);
+
+ // Assert options are initialized
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
+ defaultUnitSystem: detectDefaultUnitSystem(),
+ racePredictionOptions: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ customTargetNames: false,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
+ selectedTargetSet: '_workout_targets',
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.targetSets)
+ .to.deep.equal({ _workout_targets: defaultTargetSets._workout_targets });
+ expect(wrapper.findComponent({ name: 'single-output-table' }).vm.targets)
+ .to.deep.equal(defaultTargetSets._workout_targets.targets);
+});
+
+test('should load options from localStorage', async () => {
+ 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'
+ },
+ ],
+ },
+ };
+
+ // Initialize localStorage
+ localStorage.setItem('running-tools.global-options', JSON.stringify({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ }));
+ localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify(targetSets));
+ localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
+ customTargetNames: true,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
+
+ // Initialize component
+ const wrapper = shallowMount(WorkoutCalculator);
+
+ // Assert options are loaded
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.globalOptions).to.deep.equal({
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ },
+ });
+ expect(wrapper.findComponent({ name: 'advanced-options-input' }).vm.options).to.deep.equal({
+ customTargetNames: true,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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 options to localStorage when modified', async () => {
+ 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'
+ },
+ ],
+ },
+ };
+
+ // 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,
+ },
+ }));
+
+ // Update input race
+ await wrapper.findComponent({ name: 'pace-input' }).setValue({
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ });
+
+ // Assert data saved to localStorage
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
+ customTargetNames: false,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: '_workout_targets',
+ }));
+
+ // Update target name customization
+ await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
+ customTargetNames: true,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: '_workout_targets',
+ }, 'options');
+
+ // Assert data saved to localStorage
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(JSON.stringify({
+ customTargetNames: true,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: '_workout_targets',
+ }));
+
+ // 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({
+ customTargetNames: true,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ 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,
+ input: {
+ distanceValue: 1,
+ distanceUnit: 'miles',
+ time: 600,
+ },
+ selectedTargetSet: 'B',
+ }));
+});
test('should correctly predict workout splits', async () => {
// Initialize component
@@ -39,8 +290,11 @@ test('should correctly handle null target set', async () => {
// Switch to invalid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: 'does_not_exist',
}, 'options');
@@ -50,8 +304,11 @@ test('should correctly handle null target set', async () => {
// Switch to valid target set
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: false,
- model: 'AverageModel',
- riegelExponent: 1.06,
+ input: {
+ distanceValue: 5,
+ distanceUnit: 'kilometers',
+ time: 1200,
+ },
selectedTargetSet: '_workout_targets',
}, 'options');
@@ -61,24 +318,25 @@ test('should correctly handle null target set', async () => {
.to.deep.equal(workoutTargets);
});
-test('should correctly calculate results according to advanced model options', async () => {
+test('should correctly calculate results according to 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,
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
});
- // Update model and Riegel Exponent
+ // Update model and Riegel exponent
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
- customTargetNames: false,
- model: 'RiegelModel',
- riegelExponent: 1.10,
- selectedTargetSet: '_workout_targets',
- }, 'options');
+ defaultUnitSystem: 'imperial',
+ racePredictionOptions: {
+ model: 'RiegelModel',
+ riegelExponent: 1.10,
+ },
+ }, 'globalOptions');
// Calculate result
const calculateResult = wrapper.findComponent({ name: 'single-output-table' }).vm.calculateResult;
@@ -90,147 +348,29 @@ test('should correctly calculate results according to advanced model options', a
// Assert result is correct
expect(result.key).to.equal('1 km @ 10 km');
- expect(result.value).to.equal('4:17.23');
-});
+ expect(result.value).to.equal('3:39.23');
-test('should load input race from localStorage', async () => {
- // Initialize localStorage
- localStorage.setItem('running-tools.workout-calculator-input', JSON.stringify({
- distanceValue: 1,
- distanceUnit: 'miles',
- time: 600,
- }));
-
- // Initialize component
- const wrapper = shallowMount(WorkoutCalculator);
-
- // 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(WorkoutCalculator);
-
- // 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.workout-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(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({
- '_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': targetSet2,
- }));
- localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({
- customTargetNames: true,
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
- 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,
- 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(WorkoutCalculator);
-
- // Update options
+ // Update target name customization
await wrapper.findComponent({ name: 'advanced-options-input' }).setValue({
customTargetNames: true,
- model: 'CameronModel',
- riegelExponent: 1.3,
- selectedTargetSet: 'B',
+ input: {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ },
+ 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('3:39.23');
});
test('should correctly set AdvancedOptionsInput type prop', async () => {