running-tools

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

commit 78feba7fc21b6777e9e70fe0350ebaa3536a4c09
parent db2f7a77fc6fc5cfb2a888bceccb0d1709df9130
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Wed, 12 Jun 2024 17:34:03 -0700

Implement TargetEditor setType prop

Diffstat:
Msrc/components/TargetEditor.vue | 18+++++++++++++-----
Msrc/components/TargetSetSelector.vue | 17++++++++++++-----
Msrc/utils/targets.js | 14++++----------
Msrc/views/AboutPage.vue | 17++++++-----------
Msrc/views/SplitCalculator.vue | 5++---
Mtests/e2e/cross-calculator.spec.js | 1-
Mtests/e2e/split-calculator.spec.js | 2--
Mtests/unit/components/TargetEditor.spec.js | 19+++++++++++++++++++
Mtests/unit/components/TargetSetSelector.spec.js | 39+++++++++++++++++++++++++++------------
Mtests/unit/views/SplitCalculator.spec.js | 72------------------------------------------------------------------------
10 files changed, 83 insertions(+), 121 deletions(-)

diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue @@ -56,11 +56,9 @@ <button title="Add distance target" @click="addDistanceTarget"> Add distance target </button> - <button title="Add time target" @click="addTimeTarget"> + <button title="Add time target" @click="addTimeTarget" v-if="setType !== 'split'"> Add time target </button> - <br/> - <p>Note: time targets are ignored by the Split Calculator</p> </td> </tr> </tfoot> @@ -72,7 +70,6 @@ import { watch, ref } from 'vue'; import VueFeather from 'vue-feather'; -import targetUtils from '@/utils/targets'; import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; @@ -83,7 +80,10 @@ import TimeInput from '@/components/TimeInput.vue'; */ const model = defineModel({ type: Object, - default: JSON.parse(JSON.stringify(targetUtils.defaultTargetSet)), + default: { + name: 'New target set', + targets: [], + } }); const props = defineProps({ @@ -102,6 +102,14 @@ const props = defineProps({ type: String, default: 'metric', }, + + /** + * The target set type ('standard' or 'split') + */ + setType: { + type: String, + default: 'standard' + }, }); // Declare emitted events diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue @@ -7,15 +7,14 @@ <option value="_new">[ Create New Target Set ]</option> </select> - <button class="icon" title="Edit target set" - @click="sortTargetSet(); dialogElement.showModal()"> + <button class="icon" title="Edit target set" @click="dialogElement.showModal()"> <vue-feather type="edit" aria-hidden="true"/> </button> <dialog ref="dialogElement" class="target-set-editor-dialog" aria-label="Edit target set"> - <target-editor @close="sortTargetSet(); dialogElement.close()" v-model="targetSets[internalValue]" - @revert="revertTargetSet" :default-unit-system="defaultUnitSystem" - :isCustomSet="!internalValue.startsWith('_')"/> + <target-editor @close="sortTargetSet(); dialogElement.close()" + @revert="revertTargetSet" :default-unit-system="defaultUnitSystem" :setType="setType" + v-model="targetSets[internalValue]" :isCustomSet="!internalValue.startsWith('_')"/> </dialog> </span> </template> @@ -53,6 +52,14 @@ defineProps({ type: String, default: 'metric', }, + + /** + * The target set type ('standard' or 'split') + */ + setType: { + type: String, + default: 'standard' + }, }); /** diff --git a/src/utils/targets.js b/src/utils/targets.js @@ -19,7 +19,7 @@ function sort(targets) { const defaultTargetSets = { '_pace_targets': { name: 'Common Pace Targets', - targets: [ + targets: sort([ { type: 'distance', distanceValue: 100, distanceUnit: 'meters' }, { type: 'distance', distanceValue: 200, distanceUnit: 'meters' }, { type: 'distance', distanceValue: 300, distanceUnit: 'meters' }, @@ -54,11 +54,11 @@ const defaultTargetSets = { { type: 'time', time: 600 }, { type: 'time', time: 1800 }, { type: 'time', time: 3600 }, - ], + ]), }, '_race_targets': { name: 'Common Race Targets', - targets: [ + targets: sort([ { type: 'distance', distanceValue: 400, distanceUnit: 'meters' }, { type: 'distance', distanceValue: 800, distanceUnit: 'meters' }, { type: 'distance', distanceValue: 1500, distanceUnit: 'meters' }, @@ -77,7 +77,7 @@ const defaultTargetSets = { { type: 'distance', distanceValue: 0.5, distanceUnit: 'marathons' }, { type: 'distance', distanceValue: 1, distanceUnit: 'marathons' }, - ], + ]), }, '_split_targets': { name: '5K Mile Splits', @@ -89,13 +89,7 @@ const defaultTargetSets = { }, }; -const defaultTargetSet = { - name: 'New target set', - targets: [], -}; - export default { sort, defaultTargetSets, - defaultTargetSet, }; diff --git a/src/views/AboutPage.vue b/src/views/AboutPage.vue @@ -87,10 +87,6 @@ <li>If I finished a 5K in 20:00 and ran the first 2 miles in 13:00, how fast was the last ~1.1 miles? (6:19 per mile pace)</li> </ul> - <p> - <strong>Note:</strong> The split calculator only works with distance targets and ignores all - time targets. - </p> <h3>Unit Calculator</h3> <p> @@ -108,17 +104,16 @@ <h2>Target Sets</h2> <p> - A target set is a collection of distances and times that the Pace, Race, and Split Calculators - will calculate results for. + A target set is a collection of distances and/or times that the Pace, Race, or Split + Calculators will calculate results for. These calculators will output a duration for each distance target and a distance for each time target. - Running Tools comes with three default target sets. - You can switch between these sets, modify the targets they contain, and add new targets sets - from within each supporting calculator. + Each of these calculators comes with a default target set and allows you to add new target + sets, modify existing target sets, and switch between sets that belong to the same + calculator. </p> <p> - <strong>Note:</strong> The split calculator only works with distance targets and ignores all - time targets. + <strong>Note:</strong> The split calculator only supports distance targets. </p> </div> </template> diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -10,7 +10,7 @@ <div class="target-set"> Target Set: - <target-set-selector v-model:selectedTargetSet="selectedTargetSet" + <target-set-selector v-model:selectedTargetSet="selectedTargetSet" setType="split" v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/> </div> @@ -104,8 +104,7 @@ const results = computed(() => { // Check for missing target set if (!targetSets.value[selectedTargetSet.value]) return []; - let targets = targetUtils.sort(targetSets.value[selectedTargetSet.value].targets.filter(x => - x.type === 'distance')); + let targets = targetSets.value[selectedTargetSet.value].targets; for (let i = 0; i < targets.length; i += 1) { // Calculate split and total times diff --git a/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js @@ -59,7 +59,6 @@ test('Save and update state when navigating between calculators', async ({ page await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers'); await page.getByLabel('Target distance value').nth(1).fill('3.2'); await page.getByLabel('Target distance unit').nth(1).selectOption('Kilometers'); - await page.getByRole('button', { name: 'Add time target' }).click(); await page.getByRole('button', { name: 'Close' }).click(); // Enter input 5K splits (7:00, 6:30, 6:30) diff --git a/tests/e2e/split-calculator.spec.js b/tests/e2e/split-calculator.spec.js @@ -55,7 +55,6 @@ test('Customize target sets', async ({ page }) => { await page.getByRole('button', { name: 'Add distance target' }).click(); await page.getByLabel('Target distance value').nth(3).fill('4.8'); await page.getByLabel('Target distance unit').nth(3).selectOption('Kilometers'); - await page.getByRole('button', { name: 'Add time target' }).click(); await page.getByRole('button', { name: 'Close' }).click(); // Assert times and paces are correct (new distances are processed) @@ -102,7 +101,6 @@ test('Customize target sets', async ({ page }) => { await page.getByRole('button', { name: 'Add distance target' }).click(); await page.getByLabel('Target distance value').nth(1).fill('800'); await page.getByLabel('Target distance unit').nth(1).selectOption('Meters'); - await page.getByRole('button', { name: 'Add time target' }).click(); await page.getByRole('button', { name: 'Close' }).click(); // Assert times and paces are correct (input splits initialized to zero) diff --git a/tests/unit/components/TargetEditor.spec.js b/tests/unit/components/TargetEditor.spec.js @@ -156,6 +156,25 @@ test('add time target button should correctly add time target', async () => { ]); }); +test('add time target button should be hidden for split target sets', async () => { + // Initialize component + const wrapper = shallowMount(TargetEditor, { + propsData: { + modelValue: { + name: 'My target set', + targets: [ + { distanceUnit: 'miles', distanceValue: 1, type: 'distance' }, + { distanceUnit: 'miles', distanceValue: 2, type: 'distance' }, + ], + }, + setType: 'split', + }, + }); + + // Add time target + expect(wrapper.findAll('button[title="Add time target"]')).toHaveLength(0); +}); + test('Should emit input event when targets are updated', async () => { // Initialize component const wrapper = shallowMount(TargetEditor, { diff --git a/tests/unit/components/TargetSetSelector.spec.js b/tests/unit/components/TargetSetSelector.spec.js @@ -255,18 +255,12 @@ test('edit button should open target editor with the correct props for custom se expect(targetEditor.vm.defaultUnitSystem).to.equal('fake-unit-system'); }); -test('should sort target set before target editor is opened', async () => { +test('should sort target set after target editor is closed', async () => { // Initialize component let targetSets = { '_split_targets': { name: '5K Mile Splits', - targets: [ - { type: 'time', timeValue: 60 }, - { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, - ], + targets: [], }, }; const wrapper = shallowMount(TargetSetSelector, { @@ -276,11 +270,21 @@ test('should sort target set before target editor is opened', async () => { } }); - // Mock showModal function - wrapper.vm.dialogElement.showModal = vi.fn(); + // Mock modal close function + wrapper.vm.dialogElement.close = vi.fn(); - // Click edit button - await wrapper.find('button').trigger('click'); + // Update targets and trigger close event + await wrapper.findComponent({ name: 'target-editor' }).setValue({ + name: '5K Mile Splits', + targets: [ + { type: 'time', timeValue: 60 }, + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + { type: 'distance', distanceValue: 5, distanceUnit: 'kilometers' }, + { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, + ], + }); + await wrapper.findComponent({ name: 'target-editor' }).vm.$emit('close'); // Assert target set was sorted expect(wrapper.findComponent({ name: 'target-editor' }).vm.modelValue).to.deep.equal({ @@ -294,3 +298,14 @@ test('should sort target set before target editor is opened', async () => { ], }); }); + +test('should correctly pass setType prop to TargetEditor', async () => { + const wrapper = shallowMount(TargetSetSelector, { + propsData: { + setType: 'foo' + } + }); + + // Assert target editor props are correct + expect(wrapper.findComponent({ name: 'target-editor' }).vm.setType).to.equal('foo'); +}); diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -128,78 +128,6 @@ test('should correctly calculate paces and cumulative times from entered split t expect(rows.length).to.equal(3); }); -test('should correctly sort split targets', async () => { - // Initialize localStorage (targets are mis-ordered) - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ - '_split_targets': { - name: 'Split targets', - targets: [ - { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, - { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, - ], - }, - })); - - // 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('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 km'); - 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('2 mi'); - 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); -}); - -test('should ignore time based targets', async () => { - // Initialize localStorage - localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({ - '_split_targets': { - name: 'Split targets', - targets: [ - { type: 'distance', distanceValue: 1, distanceUnit: 'kilometers' }, - { type: 'time', time: 600 }, - { type: 'distance', distanceValue: 2, distanceUnit: 'kilometers' }, - { type: 'distance', distanceValue: 3000, distanceUnit: 'meters' }, - ], - }, - })); - // 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('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 km'); - 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('3000 m'); - 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); -}); - test('should correctly save split times with split targets in localStorage', async () => { // Initialize localStorage localStorage.setItem('running-tools.split-calculator-target-sets', JSON.stringify({