commit 8d42bd67ba4b6f6dcb5ef4586ff9c699c8ff4e58
parent 346ce417450319c09d36269aad7eb4f44975b084
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 31 May 2025 12:58:42 -0700
Automatically migrate workout calculator options
Diffstat:
4 files changed, 136 insertions(+), 15 deletions(-)
diff --git a/src/composables/useStorage.js b/src/composables/useStorage.js
@@ -1,7 +1,6 @@
import { ref, onActivated, watchEffect } from 'vue';
-// The global localStorage prefix
-const prefix = 'running-tools';
+import * as storage from '@/utils/storage';
/*
* Create a reactive value that is synced with a localStorage item
@@ -14,12 +13,7 @@ export default function useStorage(key, defaultValue) {
// (Re)load value from localStorage
function updateValue() {
- let parsedValue;
- try {
- parsedValue = JSON.parse(localStorage.getItem(`${prefix}.${key}`));
- } catch {
- parsedValue = null;
- }
+ let parsedValue = storage.get(key);
if (parsedValue !== null) value.value = parsedValue;
}
updateValue();
@@ -28,7 +22,7 @@ export default function useStorage(key, defaultValue) {
// Save value to localStorage when modified
watchEffect(() => {
if (typeof localStorage !== 'undefined') {
- localStorage.setItem(`${prefix}.${key}`, JSON.stringify(value.value));
+ storage.set(key, value.value);
}
})
diff --git a/src/main.js b/src/main.js
@@ -1,11 +1,13 @@
-import './assets/global.css';
-
import { createApp } from 'vue';
-import App from './App.vue';
-import router from './router';
-const app = createApp(App);
+import App from '@/App.vue';
+import router from '@/router';
+import * as storage from '@/utils/storage';
-app.use(router);
+import '@/assets/global.css';
+storage.migrate();
+
+const app = createApp(App);
+app.use(router);
app.mount('#app');
diff --git a/src/utils/storage.js b/src/utils/storage.js
@@ -0,0 +1,36 @@
+// The global localStorage prefix
+const prefix = 'running-tools';
+
+/**
+ * Read an object from a localStorage item
+ * @param {String} key The localStorage item's key
+ * @returns {Object} The object
+ */
+export function get(key) {
+ try {
+ return JSON.parse(localStorage.getItem(`${prefix}.${key}`));
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Write an object to a localStorage item
+ * @param {String} key The localStorage item's key
+ * @param {Object} value The object to write
+ */
+export function set(key, value) {
+ localStorage.setItem(`${prefix}.${key}`, JSON.stringify(value));
+}
+
+/**
+ * Migrate outdated localStorage options
+ */
+export function migrate() {
+ // Add customTargetNames property to workout options (>1.4.1)
+ let workoutOptions = get('workout-calculator-options');
+ if (workoutOptions !== null && workoutOptions.customTargetNames === undefined) {
+ workoutOptions.customTargetNames = false;
+ set('workout-calculator-options', workoutOptions);
+ }
+}
diff --git a/tests/unit/utils/storage.spec.js b/tests/unit/utils/storage.spec.js
@@ -0,0 +1,89 @@
+import { beforeEach, describe, test, expect } from 'vitest';
+import * as storage from '@/utils/storage';
+
+beforeEach(() => {
+ localStorage.clear();
+})
+
+describe('get method', () => {
+ test('should correctly parse correct localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Assert result is correct
+ expect(storage.get('foo')).to.deep.equal({ bar: 123 });
+ });
+
+ test('should return null for corrupt localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', 'invalid json');
+
+ // Assert result is correct
+ expect(storage.get('foo')).to.equal(null);
+ });
+
+ test('should return null for missing localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Assert result is correct
+ expect(storage.get('baz')).to.equal(null);
+ });
+});
+
+describe('set method', () => {
+ test('should correctly set new localStorage item', async () => {
+ // Set localStorage item
+ storage.set('foo', { baz: 456 });
+
+ // Assert result is correct
+ expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
+ });
+
+ test('should correctly override existing localStorage item', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.foo', '{"bar":123}');
+
+ // Set localStorage item
+ storage.set('foo', { baz: 456 });
+
+ // Assert result is correct
+ expect(localStorage.getItem('running-tools.foo')).to.equal('{"baz":456}');
+ });
+});
+
+describe('migrate method', () => {
+ test('should correctly migrate <=1.4.1 workout calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"model":"PurdyPointsModel","riegelExponent":1.1}');
+
+ // Run migratinos
+ storage.migrate();
+
+ // Assert localStorage entries correctly migrated
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"model":"PurdyPointsModel","riegelExponent":1.1,"customTargetNames":false}');
+ });
+
+ test('should not modify >1.4.1 workout calculator options', async () => {
+ // Initialize localStorage
+ localStorage.setItem('running-tools.workout-calculator-options',
+ '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.1}');
+
+ // Run migratinos
+ storage.migrate();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(
+ '{"customTargetNames":true,"model":"PurdyPointsModel","riegelExponent":1.1}');
+ });
+
+ test('should not modify missing workout calculator options', async () => {
+ // Run migratinos
+ storage.migrate();
+
+ // Assert localStorage entries not modified
+ expect(localStorage.getItem('running-tools.workout-calculator-options')).to.equal(null);
+ });
+});