running-tools

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

commit 608a6f6768a41f4b8c0e21742aeaac5b105955e9
parent 49de02063e07c29ea764fa088c034c8fd6df404e
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Thu, 13 Jun 2024 12:00:30 -0700

Extract Split Calculator logic into component

Diffstat:
Asrc/components/SplitOutputTable.vue | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/views/SplitCalculator.vue | 116+++++++++++--------------------------------------------------------------------
Atests/unit/components/SplitOutputTable.spec.js | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/unit/views/SplitCalculator.spec.js | 294+++++++++++++++++++++----------------------------------------------------------
4 files changed, 379 insertions(+), 315 deletions(-)

diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue @@ -0,0 +1,127 @@ +<template> + <table class="split-output-table"> + <thead> + <tr> + <th> + <span>Distance</span> + <span class="mobile-abbreviation">Dist.</span> + </th> + + <th>Time</th> + + <th>Split</th> + + <th>Pace</th> + </tr> + </thead> + + <tbody> + <tr v-for="(item, index) in results" :key="index"> + <td> + {{ formatUtils.formatNumber(item.distanceValue, 0, 2, false) }} + {{ unitUtils.DISTANCE_UNITS[item.distanceUnit].symbol }} + </td> + + <td> + {{ formatUtils.formatDuration(item.time, 3, 2, true) }} + </td> + + <td> + <time-input v-model="targets[index].splitTime" label="Split duration" :showHours="false"/> + </td> + + <td> + {{ formatUtils.formatDuration(item.pace, 3, 0, true) }} + / {{ unitUtils.DISTANCE_UNITS[unitUtils.getDefaultDistanceUnit(defaultUnitSystem)] + .symbol }} + </td> + </tr> + + <tr v-if="results.length === 0" class="empty-message"> + <td colspan="5"> + There aren't any targets in this set yet. + </td> + </tr> + </tbody> + </table> +</template> + +<script setup> +import { computed } from 'vue'; + +import formatUtils from '@/utils/format'; +import unitUtils from '@/utils/units'; + +import TimeInput from '@/components/TimeInput.vue'; + +/** + * The split targets + */ +const targets = defineModel({ + type: Array, + default: () => [], +}) + +const props = defineProps({ + /** + * The unit system to use when showing result paces + */ + defaultUnitSystem: { + type: String, + default: 'metric', + }, +}); + +/** + * The target table results + */ +const results = computed(() => { + // Initialize results array + const results = []; + + for (let i = 0; i < targets.value.length; i += 1) { + // Calculate split and total times + const splitTime = targets.value[i].splitTime || 0; + const totalTime = i === 0 ? splitTime : results[i - 1].time + splitTime; + + // Calculate split and total distances + const totalDistance = unitUtils.convertDistance( + targets.value[i].distanceValue, + targets.value[i].distanceUnit, 'meters', + ); + const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; + + // Calculate pace + const pace = splitTime / unitUtils.convertDistance(splitDistance, 'meters', + unitUtils.getDefaultDistanceUnit(props.defaultUnitSystem)); + + // Add row to results array + results.push({ + distance: totalDistance, + distanceValue: targets.value[i].distanceValue, + distanceUnit: targets.value[i].distanceUnit, + time: totalTime, + splitTime, + pace, + }); + } + + // Return results array + return results; +}); +</script> + +<style scoped> +/* Show/hide mobile abbreviations */ +.split-output-table th:first-child span.mobile-abbreviation { + display: none; +} +@media only screen and (max-width: 500px) { + .split-output-table th:first-child span:not(.mobile-abbreviation) { + display: none; + } + .split-output-table th:first-child span.mobile-abbreviation { + display: inherit; + } +} +</style> diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -15,52 +15,7 @@ </div> <div class="output"> - <table class="results"> - <thead> - <tr> - <th> - <span>Distance</span> - <span class="mobile-abbreviation">Dist.</span> - </th> - - <th>Time</th> - - <th>Split</th> - - <th>Pace</th> - </tr> - </thead> - - <tbody> - <tr v-for="(item, index) in results" :key="index"> - <td> - {{ formatUtils.formatNumber(item.distanceValue, 0, 2, false) }} - {{ unitUtils.DISTANCE_UNITS[item.distanceUnit].symbol }} - </td> - - <td> - {{ formatUtils.formatDuration(item.totalTime, 3, 2, true) }} - </td> - - <td v-if="targetSets[selectedTargetSet]"> - <time-input v-model="targetSets[selectedTargetSet].targets[index].split" - label="Split duration" :showHours="false"/> - </td> - - <td> - {{ formatUtils.formatDuration(item.pace, 3, 0, true) }} - / {{ unitUtils.DISTANCE_UNITS[unitUtils.getDefaultDistanceUnit(defaultUnitSystem)] - .symbol }} - </td> - </tr> - - <tr v-if="!targetSets[selectedTargetSet] || targetSets[selectedTargetSet].targets.length === 0" class="empty-message"> - <td colspan="5"> - There aren't any targets in this set yet. - </td> - </tr> - </tbody> - </table> + <split-output-table :default-unit-system="defaultUnitSystem" v-model="targetSet"/> </div> </div> </template> @@ -68,12 +23,11 @@ <script setup> import { computed } from 'vue'; -import formatUtils from '@/utils/format'; import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; +import SplitOutputTable from '@/components/SplitOutputTable.vue'; import TargetSetSelector from '@/components/TargetSetSelector.vue'; -import TimeInput from '@/components/TimeInput.vue'; import useStorage from '@/composables/useStorage'; @@ -95,46 +49,21 @@ const targetSets = useStorage('split-calculator-target-sets', { }); /** - * The target table results + * The active target set */ -const results = computed(() => { - // Initialize results array - const results = []; - - // Check for missing target set - if (!targetSets.value[selectedTargetSet.value]) return []; - - let targets = targetSets.value[selectedTargetSet.value].targets; - - 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(defaultUnitSystem.value)); - - // Add row to results array - results.push({ - distance: totalDistance, - distanceValue: targets[i].distanceValue, - distanceUnit: targets[i].distanceUnit, - totalTime, - splitTime, - pace, - }); - } - - // Return results array - return results; +const targetSet = computed({ + get: () => { + if (targetSets.value[selectedTargetSet.value]) { + return targetSets.value[selectedTargetSet.value].targets + } else { + return [] + } + }, + set: (newValue) => { + if (targetSets.value[selectedTargetSet.value]) { + targetSets.value[selectedTargetSet.value].targets = newValue; + } + }, }); </script> @@ -151,17 +80,4 @@ const results = computed(() => { min-width: 400px; } } - -/* Show/hide mobile abbreviations */ -.results th:first-child span.mobile-abbreviation { - display: none; -} -@media only screen and (max-width: 500px) { - .results th:first-child span:not(.mobile-abbreviation) { - display: none; - } - .results th:first-child span.mobile-abbreviation { - display: inherit; - } -} </style> diff --git a/tests/unit/components/SplitOutputTable.spec.js b/tests/unit/components/SplitOutputTable.spec.js @@ -0,0 +1,157 @@ +import { test, expect } from 'vitest'; +import { shallowMount } from '@vue/test-utils'; +import SplitOutputTable from '@/components/SplitOutputTable.vue'; + +test('should initialize undefined splits to 0:00.00', async () => { + // Initialize component + const wrapper = shallowMount(SplitOutputTable, { + propsData: { + modelValue: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }); + + // Assert results are correct + const rows = wrapper.findAll('tbody tr'); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); + expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); + expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); + expect(rows[0].findAll('td').length).to.equal(4); + expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); + expect(rows[1].findAll('td')[1].element.textContent).to.equal('0:00.00'); + expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); + expect(rows[1].findAll('td').length).to.equal(4); + expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); + expect(rows[2].findAll('td')[1].element.textContent).to.equal('0:00.00'); + expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly load split times from split targets', async () => { + // Initialize component + const wrapper = shallowMount(SplitOutputTable, { + propsData: { + modelValue: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', splitTime: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', splitTime: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', splitTime: 200 }, + ], + }, + }); + + // Assert results are correct + const rows = wrapper.findAll('tbody tr'); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); + expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); + expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) + .to.equal(180); + expect(rows[0].findAll('td').length).to.equal(4); + expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); + expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); + expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) + .to.equal(190); + expect(rows[1].findAll('td').length).to.equal(4); + expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); + expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); + expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) + .to.equal(200); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly calculate paces and cumulative times from entered split times', async () => { + // Initialize component + const wrapper = shallowMount(SplitOutputTable, { + propsData: { + modelValue: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }); + + // Update split times + await wrapper.findAllComponents({ name: 'time-input' })[0].setValue(420); + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(390); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(390); + + // Assert results are correct + const rows = wrapper.findAll('tbody tr'); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); + expect(rows[0].findAll('td')[1].element.textContent).to.equal('7:00.00'); + expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }) + .vm.modelValue).to.equal(420); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:21 / km'); + expect(rows[0].findAll('td').length).to.equal(4); + expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); + expect(rows[1].findAll('td')[1].element.textContent).to.equal('13:30.00'); + expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }) + .vm.modelValue).to.equal(390); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('4:02 / km'); + expect(rows[1].findAll('td').length).to.equal(4); + expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); + expect(rows[2].findAll('td')[1].element.textContent).to.equal('20:00.00'); + expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }) + .vm.modelValue).to.equal(390); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:39 / km'); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly update modelValue with split times', async () => { + // Initialize component + const wrapper = shallowMount(SplitOutputTable, { + propsData: { + modelValue: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', splitTime: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', splitTime: 180 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', splitTime: 180 }, + ], + }, + }); + + // Update split times + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(200); + + // Assert modelValue correctly updated + expect(wrapper.vm.modelValue).to.deep.equal([ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', splitTime: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', splitTime: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', splitTime: 200 }, + ]); +}); + +test('should update paces according to default units setting', async () => { + // Initialize component + const wrapper = shallowMount(SplitOutputTable, { + propsData: { + modelValue: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles', splitTime: 300 }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles', splitTime: 300 }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers', splitTime: 330 }, + ], + defaultUnitSystem: 'metric', + } + }); + + // Assert paces are correct + let rows = wrapper.findAll('tbody tr'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:06 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:06 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:05 / km'); + + // Change default units + await wrapper.setProps({ defaultUnitSystem: 'imperial' }); + + // Assert paces are correct + rows = wrapper.findAll('tbody tr'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('5:00 / mi'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:00 / mi'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('4:58 / mi'); +}); diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -6,36 +6,31 @@ beforeEach(() => { localStorage.clear(); }) -test('should initialize undefined splits to 0:00.00', async () => { +test('should load selected target set from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.split-calculator-target-set', '"B"'); + // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert results are correct - const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('0:00.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('0:00.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); - expect(rows[2].findAll('td').length).to.equal(4); - expect(rows.length).to.equal(3); + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('B'); }); -test('should correctly load split times from split targets', async () => { +test('should load targets from localStorage and pass to 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: '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 }, @@ -46,27 +41,26 @@ test('should correctly load split times from split targets', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert results are correct - const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) - .to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) - .to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue) - .to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); - expect(rows[2].findAll('td').length).to.equal(4); - expect(rows.length).to.equal(3); + // Assert default split targets are initially loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) + .to.equal('_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' }, + ]); + + // Select a new target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('B', 'selectedTargetSet'); + + // Assert new target set is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) + .to.equal('B'); + 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 }, + ]); }); test('should correctly handle null target set', async () => { @@ -76,59 +70,25 @@ test('should correctly handle null target set', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator); - // Assert results are empty - let rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent.trim()) - .to.equal('There aren\'t any targets in this set yet.'); - expect(rows[0].findAll('td').length).to.equal(1); - expect(rows.length).to.equal(1); + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('does_not_exist'); + + // Assert empty array passed to SplitOutputTable + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.modelValue).to.deep.equal([]); // Switch to valid target set await wrapper.findComponent({ name: 'target-set-selector' }) .setValue('_split_targets', 'selectedTargetSet'); - // Assert results are correct - rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); - expect(rows.length).to.equal(3); -}); - -test('should correctly calculate paces and cumulative times from entered split times', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator); - - // Update split times - await wrapper.findAllComponents({ name: 'time-input' })[0].setValue(420); - await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(390); - await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(390); - - // Assert results are correct - const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('7:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(420); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('7:00 / mi'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('13:30.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(390); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('6:30 / mi'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('20:00.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(390); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:52 / mi'); - expect(rows[2].findAll('td').length).to.equal(4); - expect(rows.length).to.equal(3); + // Assert non-empty target set passed to SplitOutputTable + 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' }, + ]); }); -test('should correctly save split times with split targets in localStorage', 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': { @@ -145,8 +105,11 @@ test('should correctly save split times with split targets in localStorage', asy const wrapper = shallowMount(SplitCalculator); // Update split times - await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); - await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(200); + 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({ @@ -161,113 +124,6 @@ test('should correctly save split times with split targets in localStorage', asy })); }); -test('should update results when a new target set is selected', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ - '_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 }, - ], - }, - })); - - // Initialize component - const wrapper = shallowMount(SplitCalculator); - - // Assert default split targets are initially loaded - expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet) - .to.equal('_split_targets'); - expect(wrapper.findAll('tbody td')[0].element.textContent).to.equal('1 mi'); - - // Select a new target set - await wrapper.findComponent({ name: 'target-set-selector' }).setValue('B', 'selectedTargetSet'); - - // Assert results are correct - const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); - expect(rows[2].findAll('td').length).to.equal(4); - expect(rows.length).to.equal(3); -}); - -test('should load selected target set from localStorage', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-set', '"B"'); - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ - '_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 }, - ], - }, - })); - - - // Initialize component - const wrapper = shallowMount(SplitCalculator); - - // Assert selection is loaded - expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('B'); - - // Assert results are correct - const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }) - .vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); - expect(rows[2].findAll('td').length).to.equal(4); - expect(rows.length).to.equal(3); -}); - test('should save selected target set to localStorage when modified', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator); @@ -281,41 +137,49 @@ test('should save selected target set to localStorage when modified', async () = .to.equal('"_race_targets"'); }); -test('should update paces according to default units setting', async () => { +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); - // Enter split times - await wrapper.findAllComponents({ name: 'time-input' })[0].setValue(300); - await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(300); - await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(330); + // Assert default units setting is initialy loaded + expect(wrapper.find('select', { name: 'Default units' }).element.value).to.equal('metric'); - // Set default units setting - await wrapper.find('select', { name: 'Default units' }).setValue('metric'); - - // Assert paces are correct - let rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:06 / km'); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:06 / km'); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:05 / km'); + // Assert prop is correct + expect(wrapper.findComponent({ name: 'split-output-table' }).vm.defaultUnitSystem) + .to.equal('metric'); // Change default units await wrapper.find('select').setValue('imperial'); - // Assert paces are correct - rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('5:00 / mi'); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:00 / mi'); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('4:58 / mi'); + // 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); - // Change default units + // Set default units setting + await wrapper.find('select').setValue('metric'); + + // 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.find('select').setValue('imperial'); // New default units should be saved to localStorage expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); }); + +test('should correctly set targetSetSelector setType prop', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator); + + // Assert setType prop is correctly set + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.setType).to.equal('split'); +});