running-tools

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

commit 2f27b105dedb31218d0d649b2548c9dbf2a6814b
parent 454a7aa31f47169c9a4027affc5e7cb572023f3a
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Wed, 27 Dec 2023 11:04:12 -0800

Implement TargetSetSelector component

Diffstat:
Msrc/assets/global.css | 1+
Msrc/components/TargetEditor.vue | 28++++++++++++++++++++++++++++
Dsrc/components/TargetSetEditor.vue | 217-------------------------------------------------------------------------------
Asrc/components/TargetSetSelector.vue | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/views/PaceCalculator.vue | 38++++++--------------------------------
Msrc/views/RaceCalculator.vue | 29++++++-----------------------
Msrc/views/SplitCalculator.vue | 84++++++++++++++++++++++++++++++++-----------------------------------------------
Dtests/unit/components/TargetSetEditor.spec.js | 154-------------------------------------------------------------------------------
Atests/unit/components/TargetSetSelector.spec.js | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 388 insertions(+), 476 deletions(-)

diff --git a/src/assets/global.css b/src/assets/global.css @@ -54,6 +54,7 @@ table .empty-message svg { /* styles for icons */ .icon { + height: 2em; border: none; padding: 0em; background-color: #00000000; diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue @@ -5,6 +5,10 @@ <th> Edit <input v-model="internalValue.name" placeholder="Target set name"/> + <button class="icon" :title="isCustomSet ? 'Delete Target Set' : 'Revert Target Set'" + @click="revert" v-blur> + <vue-feather :type="isCustomSet ? 'trash-2' : 'rotate-ccw'"/> + </button> </th> <th> @@ -54,6 +58,8 @@ <button v-if="timeTargets" title="Add Time Target" @click="addTimeTarget" v-blur> Add time target </button> + <br/> + <p>Note: time targets are ignored by the Split Calculator</p> </td> </tr> </tfoot> @@ -100,6 +106,14 @@ export default { type: Boolean, default: true, }, + + /** + * Whether the target set is a custom or default set + */ + isCustomSet: { + type: Boolean, + default: false, + }, }, data() { @@ -142,6 +156,14 @@ export default { methods: { /** + * Revert the target set + */ + revert() { + // Emit revert event + this.$emit('revert'); + }, + + /** * Close the target editor */ close() { @@ -183,6 +205,9 @@ export default { <style scoped> /* edit targets table */ +.target-editor th .icon { + margin-left: 0.3em; +} .target-editor th:last-child, .target-editor td:last-child { text-align: right; } @@ -197,4 +222,7 @@ export default { .target-editor tfoot button { margin: 0.5em; } +.target-editor tfoot p { + margin-top: 0.5em; +} </style> diff --git a/src/components/TargetSetEditor.vue b/src/components/TargetSetEditor.vue @@ -1,217 +0,0 @@ -<template> - <div class="target-set-editor"> - <table v-show="selectedTargetSet === null"> - <thead> - <tr> - <th> - Edit Target Sets - </th> - <th> - <button class="icon" title="Restore Default Sets" @click="reset" v-blur> - <vue-feather type="rotate-ccw"/> - </button> - <button class="icon" title="Close" @click="close" v-blur> - <vue-feather type="x"/> - </button> - </th> - </tr> - </thead> - - <tbody> - <tr v-for="(item, key) in internalValue" :key="key"> - <td> - {{ item.name }} - </td> - <td> - <button class="icon" title="Edit Set" @click="editTargetSet(key)" v-blur> - <vue-feather type="edit"/> - </button> - <button class="icon" title="Delete Set" @click="removeTargetSet(key)" v-blur> - <vue-feather type="trash-2"/> - </button> - </td> - </tr> - - <tr v-if="Object.keys(internalValue).length === 0" class="empty-message"> - <td colspan="2"> - There aren't any target sets yet - </td> - </tr> - </tbody> - - <tfoot> - <tr> - <td colspan="2"> - <button title="Add Target Set" @click="addTargetSet" v-blur> - Add Target Set - </button> - </td> - </tr> - </tfoot> - </table> - - <target-editor v-model="internalValue[selectedTargetSet]" v-if="selectedTargetSet !== null" - @close="selectedTargetSet = null"/> - - <p v-if="selectedTargetSet !== null"> - Note: time targets are ignored by the Split Calculator - </p> - </div> -</template> - -<script> -import VueFeather from 'vue-feather'; - -import storage from '@/utils/localStorage'; -import targetUtils from '@/utils/targets'; -import unitUtils from '@/utils/units'; - -import TargetEditor from '@/components/TargetEditor.vue'; - -import blur from '@/directives/blur'; - -export default { - name: 'TargetSetEditor', - - components: { - TargetEditor, - VueFeather, - }, - - directives: { - blur, - }, - - data() { - return { - /** - * The internal value - */ - internalValue: storage.get('target-sets', targetUtils.defaultTargetSets), - - /** - * The target set currently being edited - */ - selectedTargetSet: null, - - /** - * The distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - }; - }, - - watch: { - /** - * Save target sets - */ - internalValue: { - deep: true, - handler(newValue) { - storage.set('target-sets', newValue); - }, - }, - - /** - * Sort current target set - */ - selectedTargetSet(newValue, oldValue) { - let value = newValue !== null ? newValue : oldValue; - this.internalValue[value].targets = targetUtils.sort(this.internalValue[value].targets); - }, - }, - - methods: { - /** - * Restore the default target sets - */ - reset() { - let old_sets = this.internalValue; - this.internalValue = JSON.parse(JSON.stringify(targetUtils.defaultTargetSets)); - for (let key in old_sets) { - if (!Object.keys(this.internalValue).includes(key)) { - this.internalValue[key] = old_sets[key]; - } - } - }, - - /** - * Close the target editor - */ - close() { - // Emit close event - this.$emit('close'); - }, - - /** - * Add a new target set - */ - addTargetSet() { - let key = Date.now().toString() - this.internalValue[key] = { - name: 'New target set', - targets: [], - } - this.editTargetSet(key); - }, - - /** - * Edit a target set - */ - editTargetSet(key) { - this.selectedTargetSet = key; - }, - - /** - * Remove a target set - */ - removeTargetSet(key) { - delete this.internalValue[key] - }, - }, - - /** - * Close target editor - */ - deactivated() { - this.selectedTargetSet = null; - }, -}; -</script> - -<style scoped> -/* container */ -.target-set-editor { - display: flex; - flex-direction: column; - align-items: center; -} -.target-set-editor table { - max-width: 500px; -} -h2 { - font-size: 1.3em; - margin-bottom: 0.2em; -} - -/* tables */ -.target-set-editor table th:last-child, .target-set-editor table td:last-child { - text-align: right; -} -.target-set-editor table td select { - margin-left: 0.2em; - width: 8em; -} -.target-set-editor table tfoot td { - text-align: center !important; - padding: 0.5em 0.2em; -} -.target-set-editor table tfoot button { - margin: 0.5em; -} - -/* note about split calculator */ -.target-set-editor p { - margin-top: 0.5em; -} -</style> diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue @@ -0,0 +1,149 @@ +<template> + <span class="target-set-selector"> + <select v-model="internalValue"> + <option v-for="(item, index) in targetSets" :key="index" :value="index"> + {{ item.name }} + </option> + <option value="_new">[ Create New Target Set ]</option> + </select> + + <button class="icon" title="Edit Target Set" @click="editingTargetSets = true" v-blur> + <vue-feather type="edit"/> + </button> + + <fullscreen-modal v-show="editingTargetSets"> + <target-editor @close="editingTargetSets = false" v-model="targetSets[internalValue]" + @revert="revertTargetSet(); editingTargetSets = false" + :isCustomSet="!internalValue.startsWith('_')"/> + </fullscreen-modal> + </span> +</template> + +<script> +import VueFeather from 'vue-feather'; + +import storage from '@/utils/localStorage'; +import targetUtils from '@/utils/targets'; + +import FullscreenModal from '@/components/FullscreenModal.vue'; +import TargetEditor from '@/components/TargetEditor.vue'; + +import blur from '@/directives/blur'; + +export default { + name: 'TargetSetSelector', + + components: { + FullscreenModal, + TargetEditor, + VueFeather, + }, + + directives: { + blur, + }, + + props: { + /** + * The selected target set + */ + modelValue: { + type: String, + default: '_new', + }, + }, + + data() { + return { + /** + * The internal value + */ + internalValue: this.modelValue, + + /** + * The target sets + */ + targetSets: storage.get('target-sets', targetUtils.defaultTargetSets), + + /** + * Whether the target set is being edited + */ + editingTargetSets: false, + }; + }, + + watch: { + /** + * Update the component value when the modelValue prop changes + */ + modelValue(newValue) { + if (newValue !== this.internalValue) { + this.internalValue = newValue; + } + }, + + /** + * Emit the input event when the component value changes and create a new set if necessary + */ + internalValue: { + immediate: true, + handler(newValue) { + if (newValue == '_new') { + let key = Date.now().toString(); + this.targetSets[key] = { + name: 'New target set', + targets: [], + }; + this.internalValue = key; + } else { + this.$emit('update:modelValue', newValue); + } + }, + }, + + /** + * Save target sets + */ + targetSets: { + deep: true, + handler(newValue) { + storage.set('target-sets', newValue); + this.$emit('targets-updated'); + }, + }, + + /** + * Sort target set + */ + editingTargetSets(newValue) { + if (!newValue) { + this.targetSets[this.internalValue].targets = + targetUtils.sort(this.targetSets[this.internalValue].targets); + } + }, + }, + + methods: { + /** + * Revert or remove the current target set + */ + revertTargetSet() { + if (this.internalValue.startsWith('_')) { + // Revert default set + this.targetSets[this.internalValue] = + JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[this.internalValue])); + } else { + // Remove custom set + delete this.targetSets[this.internalValue]; + this.internalValue = [...Object.keys(this.targetSets), '_new'][0]; + } + }, + }, +}; +</script> + +<style scoped> +.target-set-selector .icon { + margin-left: 0.3em; +} +</style> diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -21,37 +21,23 @@ <h2>Equivalent Paces</h2> <div class="target-set"> Target Set: - <select v-model="selectedTargetSet"> - <option v-for="(item, index) in targetSets" :key="index" :value="index"> - {{ item.name }} - </option> - </select> - <button class="icon" title="Edit Target Sets" @click="editingTargetSets = true" v-blur> - <vue-feather type="edit"/> - </button> + <target-set-selector v-model="selectedTargetSet" @targets-updated="reloadTargets"/> </div> <simple-target-table class="output" :calculate-result="calculatePace" :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/> - - <fullscreen-modal v-show="editingTargetSets"> - <target-set-editor @close="editingTargetSets = false"/> - </fullscreen-modal> </div> </template> <script> -import VueFeather from 'vue-feather'; - import paceUtils from '@/utils/paces'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; -import FullscreenModal from '@/components/FullscreenModal.vue'; import SimpleTargetTable from '@/components/SimpleTargetTable.vue'; -import TargetSetEditor from '@/components/TargetSetEditor.vue'; +import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; import blur from '@/directives/blur'; @@ -61,11 +47,9 @@ export default { components: { DecimalInput, - FullscreenModal, SimpleTargetTable, - TargetSetEditor, + TargetSetSelector, TimeInput, - VueFeather, }, directives: { @@ -139,15 +123,6 @@ export default { selectedTargetSet(newValue) { storage.set('pace-calculator-target-set', newValue); }, - - /** - * Refresh the target sets - */ - editingTargetSets(newValue) { - if (!newValue) { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - } - } }, computed: { @@ -162,11 +137,10 @@ export default { methods: { /** - * Restore the default target set + * Reload the target sets */ - resetTargetSet() { - this.targetSets[this.selectedTargetSet] = - JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[this.selectedTargetSet])); + reloadTargets() { + this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); }, /** diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -55,28 +55,15 @@ <h2>Equivalent Race Results</h2> <div class="target-set"> Target Set: - <select v-model="selectedTargetSet"> - <option v-for="(item, index) in targetSets" :key="index" :value="index"> - {{ item.name }} - </option> - </select> - <button class="icon" title="Edit Target Sets" @click="editingTargetSets = true" v-blur> - <vue-feather type="edit"/> - </button> + <target-set-selector v-model="selectedTargetSet" @targets-updated="reloadTargets"/> </div> <simple-target-table class="output" :calculate-result="predictResult" :targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []" show-pace/> - - <fullscreen-modal v-show="editingTargetSets"> - <target-set-editor @close="editingTargetSets = false"/> - </fullscreen-modal> </div> </template> <script> -import VueFeather from 'vue-feather'; - import formatUtils from '@/utils/format'; import raceUtils from '@/utils/races'; import storage from '@/utils/localStorage'; @@ -84,9 +71,8 @@ import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; -import FullscreenModal from '@/components/FullscreenModal.vue'; import SimpleTargetTable from '@/components/SimpleTargetTable.vue'; -import TargetSetEditor from '@/components/TargetSetEditor.vue'; +import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; import blur from '@/directives/blur'; @@ -96,11 +82,9 @@ export default { components: { DecimalInput, - FullscreenModal, SimpleTargetTable, - TargetSetEditor, + TargetSetSelector, TimeInput, - VueFeather, }, directives: { @@ -168,11 +152,10 @@ export default { methods: { /** - * Restore the default target set + * Reload the target sets */ - resetTargetSet() { - this.targetSets[this.selectedTargetSet].targets = - JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[this.selectedTargetSet].targets)); + reloadTargets() { + this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); }, /** diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -2,14 +2,7 @@ <div class="calculator"> <div class="target-set"> Target Set: - <select v-model="selectedTargetSet"> - <option v-for="(item, index) in targetSets" :key="index" :value="index"> - {{ item.name }} - </option> - </select> - <button class="icon" title="Edit Target Sets" @click="editingTargetSets = true" v-blur> - <vue-feather type="edit"/> - </button> + <target-set-selector v-model="selectedTargetSet" @targets-updated="reloadTargets"/> </div> <div class="output"> @@ -58,23 +51,16 @@ </tbody> </table> </div> - - <fullscreen-modal v-show="editingTargetSets"> - <target-set-editor @close="editingTargetSets = false"/> - </fullscreen-modal> </div> </template> <script> -import VueFeather from 'vue-feather'; - import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; -import FullscreenModal from '@/components/FullscreenModal.vue'; -import TargetSetEditor from '@/components/TargetSetEditor.vue'; +import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; import blur from '@/directives/blur'; @@ -83,10 +69,8 @@ export default { name: 'SplitCalculator', components: { - FullscreenModal, - TargetSetEditor, + TargetSetSelector, TimeInput, - VueFeather, }, directives: { @@ -161,33 +145,34 @@ export default { // Check for missing target set if (!this.targetSets[this.selectedTargetSet]) return []; - for (let i = 0; i < this.targetSets[this.selectedTargetSet].targets.length; i += 1) { - if (this.targetSets[this.selectedTargetSet].targets[i].result === 'time') { - // Calculate split and total times - const splitTime = this.targetSets[this.selectedTargetSet].targets[i].split || 0; - const totalTime = i === 0 ? splitTime : results[i - 1].totalTime + splitTime; - - // Calculate split and total distances - const totalDistance = unitUtils.convertDistance( - this.targetSets[this.selectedTargetSet].targets[i].distanceValue, - this.targetSets[this.selectedTargetSet].targets[i].distanceUnit, 'meters', - ); - const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; - - // Calculate pace - const pace = splitTime / unitUtils.convertDistance(splitDistance, 'meters', - unitUtils.getDefaultDistanceUnit()); - - // Add row to results array - results.push({ - distance: totalDistance, - distanceValue: this.targetSets[this.selectedTargetSet].targets[i].distanceValue, - distanceUnit: this.targetSets[this.selectedTargetSet].targets[i].distanceUnit, - totalTime, - splitTime, - pace, - }); - } + let targets = this.targetSets[this.selectedTargetSet].targets.filter(x => x.result === + 'time'); + + for (let i = 0; i < targets.length; i += 1) { + // Calculate split and total times + const splitTime = targets[i].split || 0; + const totalTime = i === 0 ? splitTime : results[i - 1].totalTime + splitTime; + + // Calculate split and total distances + const totalDistance = unitUtils.convertDistance( + targets[i].distanceValue, + targets[i].distanceUnit, 'meters', + ); + const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; + + // Calculate pace + const pace = splitTime / unitUtils.convertDistance(splitDistance, 'meters', + unitUtils.getDefaultDistanceUnit()); + + // Add row to results array + results.push({ + distance: totalDistance, + distanceValue: targets[i].distanceValue, + distanceUnit: targets[i].distanceUnit, + totalTime, + splitTime, + pace, + }); } // Return results array @@ -197,11 +182,10 @@ export default { methods: { /** - * Restore the default target set + * Reload the target sets */ - resetTargetSet() { - this.targetSets[this.selectedTargetSet] = - JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[this.selectedTargetSet])); + reloadTargets() { + this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); }, }, diff --git a/tests/unit/components/TargetSetEditor.spec.js b/tests/unit/components/TargetSetEditor.spec.js @@ -1,154 +0,0 @@ -/* eslint-disable no-underscore-dangle */ - -import { test, expect } from 'vitest'; -import { mount } from '@vue/test-utils'; -import TargetSetEditor from '@/components/TargetSetEditor.vue'; -import targetUtils from '@/utils/targets'; - -test('addTargetSet method should correctly add target set', async () => { - // Initialize component - const wrapper = mount(TargetSetEditor, { - data() { - return { - internalValue: { - 'A': { - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }, - }, - }; - }, - }); - - // Add target set - await wrapper.vm.addTargetSet(); - - // Assert target set was added - expect(wrapper.vm.internalValue['A']).to.deep.equal({ - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }); - const keys = Object.keys(wrapper.vm.internalValue) - expect(keys.length).to.equal(2); - expect(wrapper.vm.internalValue[keys[1]]).to.deep.equal({ - name: 'New target set', - targets: [], - }); - - // Assert new target set was selected - expect(wrapper.vm.selectedTargetSet).to.equal(keys[1]); -}); - -test('reset method should correctly reset target sets', async () => { - // Initialize component - const wrapper = mount(TargetSetEditor, { - data() { - return { - internalValue: { - 'A': { - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }, - '_split_targets': { - name: '5K Kilometer Splits', - targets: [ - { result: 'time', distanceValue: 2, distanceUnit: 'Kilometer' }, - { result: 'time', distanceValue: 4, distanceUnit: 'Kilometer' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - }, - }; - }, - }); - - // Reset target sets - await wrapper.vm.reset(); - - // Assert target sets were reset - expect(wrapper.vm.internalValue).to.deep.equal({ - // Default target sets should be restored - ...targetUtils.defaultTargetSets, - - // Custom target sets should be kept - 'A': { - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }, - }); -}); - -test('removeTargetSet method should correctly remove target set', async () => { - // Initialize component - const wrapper = mount(TargetSetEditor, { - data() { - return { - internalValue: { - 'A': { - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }, - 'B': { - name: '2nd target set', - targets: [ - { result: 'time', distanceValue: 4, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 6, distanceUnit: 'miles' }, - ], - }, - 'C': { - name: '3rd target set', - targets: [ - { result: 'time', distanceValue: 7, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 8, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 9, distanceUnit: 'miles' }, - ], - }, - }, - }; - }, - }); - - // Remove 2nd target set - await wrapper.vm.removeTargetSet('B'); - - // Assert target set was removed - expect(wrapper.vm.internalValue).to.deep.equal({ - 'A': { - name: '1st target set', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - ], - }, - 'C': { - name: '3rd target set', - targets: [ - { result: 'time', distanceValue: 7, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 8, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 9, distanceUnit: 'miles' }, - ], - }, - }); -}); diff --git a/tests/unit/components/TargetSetSelector.spec.js b/tests/unit/components/TargetSetSelector.spec.js @@ -0,0 +1,164 @@ +/* eslint-disable no-underscore-dangle */ + +import { test, expect } from 'vitest'; +import { mount } from '@vue/test-utils'; +import TargetSetSelector from '@/components/TargetSetSelector.vue'; + +test('Create New Target Set option should correctly add target set', async () => { + // Initialize component + const wrapper = mount(TargetSetSelector, { + data() { + return { + internalValue: 'A', + editingTargetSets: false, + targetSets: { + 'A': { + name: '1st target set', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + ], + }, + }, + }; + }, + }); + + // Add target set + wrapper.vm.internalValue = '_new'; + await wrapper.vm.$nextTick(); + + // Assert target set was added + expect(wrapper.vm.targetSets['A']).to.deep.equal({ + name: '1st target set', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + ], + }); + const keys = Object.keys(wrapper.vm.targetSets) + expect(keys.length).to.equal(2); + expect(wrapper.vm.targetSets[keys[1]]).to.deep.equal({ + name: 'New target set', + targets: [], + }); + + // Assert new target set was selected + expect(wrapper.vm.internalValue).to.equal(keys[1]); +}); + +test('revertTargetSet method should correctly reset target sets', async () => { + // Initialize component + const wrapper = mount(TargetSetSelector, { + data() { + return { + internalValue: 'A', + editingTargetSets: false, + targetSets: { + 'A': { + name: '1st target set', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + ], + }, + '_split_targets': { + name: '5K Kilometer Splits', + targets: [ + { result: 'time', distanceValue: 2, distanceUnit: 'Kilometer' }, + { result: 'time', distanceValue: 4, distanceUnit: 'Kilometer' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }, + }; + }, + }); + + // Revert first target set + await wrapper.vm.revertTargetSet(); + + // Assert first target set was removed + expect(wrapper.vm.targetSets).to.deep.equal({ + '_split_targets': { + name: '5K Kilometer Splits', + targets: [ + { result: 'time', distanceValue: 2, distanceUnit: 'Kilometer' }, + { result: 'time', distanceValue: 4, distanceUnit: 'Kilometer' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }); + expect(wrapper.vm.internalValue).to.equal('_split_targets'); + + // Revert second target set + await wrapper.vm.revertTargetSet(); + + // Assert second target set was reset + expect(wrapper.vm.targetSets).to.deep.equal({ + '_split_targets': { + name: '5K Mile Splits', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }); + expect(wrapper.vm.internalValue).to.equal('_split_targets'); +}); + +test('Target sets should be correctly sorted', async () => { + // Initialize component + const wrapper = mount(TargetSetSelector, { + data() { + return { + internalValue: '_split_targets', + editingTargetSets: false, + targetSets: { + '_split_targets': { + name: '5K Mile Splits', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }, + }; + }, + }); + + // Edit target set + wrapper.vm.editingTargetSets = true; + await wrapper.vm.$nextTick(); + wrapper.vm.targetSets['_split_targets'] = { + name: '5K Mile Splits', + targets: [ + { result: 'distance', timeValue: 60 }, + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + ], + }; + wrapper.vm.editingTargetSets = false; + await wrapper.vm.$nextTick(); + + // Assert target set was sorted + expect(wrapper.vm.targetSets).to.deep.equal({ + '_split_targets': { + name: '5K Mile Splits', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + { result: 'distance', timeValue: 60 }, + ], + }, + }); +});