commit 7ba09be38250b4360d800309a9dc15b0612fc117
parent 0ad27a6907f79aaebb3cf7a8c7d9c5e2c71a79a8
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 23 Aug 2025 14:20:43 -0700
Update localStorage migrations and related tests
Diffstat:
6 files changed, 465 insertions(+), 190 deletions(-)
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/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/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js
@@ -726,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(() =>
@@ -807,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(() =>
@@ -851,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',
@@ -861,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')))
@@ -888,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();
@@ -931,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);
@@ -944,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);
@@ -957,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');
@@ -978,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');
@@ -988,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');
@@ -997,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');
@@ -1017,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/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/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');
});
});