running-tools

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

commit 2decd85c70b0d1cd43d31ebec1b923af520f2f93
parent de99040e38f6fd15376fdb547ebcfeb313a788f9
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Mon, 25 Mar 2024 14:50:49 -0700

Improve view tests

Diffstat:
Mtests/unit/views/PaceCalculator.spec.js | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mtests/unit/views/RaceCalculator.spec.js | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mtests/unit/views/SplitCalculator.spec.js | 504++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtests/unit/views/UnitCalculator.spec.js | 3---
4 files changed, 920 insertions(+), 83 deletions(-)

diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js @@ -1,23 +1,23 @@ -/* eslint-disable no-underscore-dangle */ - -import { test, expect } from 'vitest'; +import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import PaceCalculator from '@/views/PaceCalculator.vue'; -import unitUtils from '@/utils/units'; -test('should correctly calculate times', async () => { +beforeEach(() => { + localStorage.clear(); +}) + +test('should correctly calculate time results', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); - // Override input values - await wrapper.setData({ - inputDistance: 1, - inputUnit: 'kilometers', - inputTime: 100, - }); + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(1); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); + await wrapper.findComponent({ name: 'time-input' }).setValue(100); - // Calculate paces - const result = wrapper.vm.calculatePace({ + // Calculate result + const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; + const result = calculateResult({ distanceValue: 20, distanceUnit: 'meters', result: 'time', @@ -32,28 +32,172 @@ test('should correctly calculate times', async () => { }); }); -test('should correctly calculate distances', async () => { +test('should correctly calculate distance results according to default units setting', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator, { + data() { + return { + defaultUnitSystem: 'metric', + }; + }, + }); + + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(2); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('miles'); + await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + + // Get calculate result function + const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; + + // Assert result is correct + let result = calculateResult({ result: 'distance', time: 600 }); + expect(result.distanceValue).to.be.closeTo(1.609, 0.001); + expect(result.distanceUnit).to.equal('kilometers'); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // Assert result is correct + result = calculateResult({ result: 'distance', time: 600 }); + expect(result.distanceValue).to.equal(1); + expect(result.distanceUnit).to.equal('miles'); + expect(result.time).to.equal(600); + expect(result.result).to.equal('distance'); +}); + +test('should not show paces in results table', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); - // Override input values - await wrapper.setData({ - inputDistance: 1, - inputUnit: 'miles', - inputTime: 100, + // Assert paces are not shown in results table + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.showPace).to.equal(false); +}); + +test('should correctly handle null target set', async () => { + // Initialize component + const paceTargets = [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ]; + const wrapper = shallowMount(PaceCalculator, { + data() { + return { + targetSets: { + '_pace_targets': { + name: 'Common pace targets', + targets: paceTargets, + }, + '_race_targets': null, + }, + }; + }, }); - // Calculate paces - const result = wrapper.vm.calculatePace({ - time: 200, - result: 'distance', + // Switch to invalid target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); + + // Assert empty array passed to SimpleTargetTable component + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal([]); + + // Switch to valid target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_pace_targets'); + + // Assert valid targets passed to SimpleTargetTable component + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(paceTargets); +}); + +test('should load input pace from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.pace-calculator-input-distance', '1'); + localStorage.setItem('running-tools.pace-calculator-input-unit', '"miles"'); + localStorage.setItem('running-tools.pace-calculator-input-time', '600'); + + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Assert data loaded + expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1); + expect(wrapper.find('select[aria-label="Input distance unit"]').element.value).to.equal('miles'); + expect(wrapper.findComponent({ name: 'time-input' }).vm.modelValue).to.equal(600); +}); + +test('should save input pace to localStorage', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(1); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('miles'); + await wrapper.findComponent({ name: 'time-input' }).setValue(600); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.pace-calculator-input-distance')).to.equal('1'); + expect(localStorage.getItem('running-tools.pace-calculator-input-unit')).to.equal('"miles"'); + expect(localStorage.getItem('running-tools.pace-calculator-input-time')).to.equal('600'); +}); + +test('should load selected target set from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.pace-calculator-target-set', '"_race_targets"'); + + // Initialize component + const raceTargets = [ + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 6, distanceUnit: 'kilometers' }, + ]; + const wrapper = shallowMount(PaceCalculator, { + data() { + return { + targetSets: { + '_pace_targets': { + name: 'Common pace targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + '_race_targets': { + name: 'Common race targets', + targets: raceTargets, + }, + }, + }; + }, }); - // Assert result is correct - expect(result).to.deep.equal({ - distanceValue: unitUtils.convertDistance(2, 'miles', unitUtils.getDefaultDistanceUnit(unitUtils.detectDefaultUnitSystem())), - distanceUnit: unitUtils.getDefaultDistanceUnit(unitUtils.detectDefaultUnitSystem()), - time: 200, - result: 'distance', + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('_race_targets'); + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(raceTargets); +}); + +test('should save selected target set to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator); + + // Select a new target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); + + // New selected target set should be saved to localStorage + expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal('"_race_targets"'); +}); + +test('should save default units setting to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(PaceCalculator, { + data() { + return { + defaultUnitSystem: 'metric', + }; + }, }); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); }); diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -1,63 +1,289 @@ -/* eslint-disable no-underscore-dangle */ - -import { test, expect } from 'vitest'; +import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; -import raceUtils from '@/utils/races'; -import unitUtils from '@/utils/units'; import RaceCalculator from '@/views/RaceCalculator.vue'; +beforeEach(() => { + localStorage.clear(); +}) + test('should correctly predict race times', async () => { // Initialize component const wrapper = shallowMount(RaceCalculator); - // Override input values - await wrapper.setData({ - inputDistance: 5, - inputUnit: 'kilometers', - inputTime: 1200, + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(5); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); + await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + + // Calculate result + const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; + const result = calculateResult({ + distanceValue: 10, + distanceUnit: 'kilometers', + result: 'time', + }); + + // Assert result is correct + expect(result.time).to.be.closeTo(2495, 1); + expect(result.distanceValue).to.equal(10); + expect(result.distanceUnit).to.equal('kilometers'); + expect(result.result).to.equal('time'); +}); + +test('should correctly calculate distance results according to default units setting', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator, { + data() { + return { + defaultUnitSystem: 'metric', + }; + }, }); - // Predict race times - const result = wrapper.vm.predictResult({ + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(5); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); + await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + + // Get calculate result function + const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; + + // Assert result is correct + let result = calculateResult({ result: 'distance', time: 2495 }); + expect(result.distanceValue).to.be.closeTo(10, 0.01); + expect(result.distanceUnit).to.equal('kilometers'); + expect(result.time).to.equal(2495); + expect(result.result).to.equal('distance'); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // Assert result is correct + result = calculateResult({ result: 'distance', time: 2495 }); + expect(result.distanceValue).to.be.closeTo(6.214, 0.01); + expect(result.distanceUnit).to.equal('miles'); + expect(result.time).to.equal(2495); + expect(result.result).to.equal('distance'); +}); + +test('should show paces in results table', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Assert paces are shown in results table + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.showPace).to.equal(true); +}); + +test('should correctly handle null target set', async () => { + // Initialize component + const raceTargets = [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ]; + const wrapper = shallowMount(RaceCalculator, { + data() { + return { + targetSets: { + '_pace_targets': null, + '_race_targets': { + name: 'Common race targets', + targets: raceTargets, + }, + }, + }; + }, + }); + + // Switch to invalid target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_pace_targets'); + + // Assert empty array passed to SimpleTargetTable component + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal([]); + + // Switch to valid target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); + + // Assert valid targets passed to SimpleTargetTable component + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(raceTargets); +}); + +test('should correctly calculate race statistics', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(5); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); + await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + + // Get race statistics + const raceStats = wrapper.findAll('details')[0]; + const purdyPoints = raceStats.findAll('div')[0].element.textContent.trim(); + const vo2 = raceStats.findAll('div')[1].element.textContent.trim(); + const vo2Max = raceStats.findAll('div')[2].element.textContent.trim(); + + // Assert race statistics are correct + expect(purdyPoints).to.equal('Purdy Points: 454.5'); + expect(vo2).to.equal('V̇O₂: 47.4 ml/kg/min (95.3% of max)') + expect(vo2Max).to.equal('V̇O₂ Max: 49.8 ml/kg/min') +}); + +test('should correctly calculate results according to advanced model options', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(5); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); + await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + + // Switch model + await wrapper.find('select[aria-label="Prediction model"]').setValue('RiegelModel'); + + // Calculate result + const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; + let result = calculateResult({ distanceValue: 10, distanceUnit: 'kilometers', result: 'time', }); // Assert result is correct - const prediction = raceUtils.AverageModel.predictTime(5000, 1200, 10000); - expect(result).to.deep.equal({ + expect(result.time).to.be.closeTo(2502, 1); + + // Update Riegel Exponent + expect(wrapper.findComponent('[aria-label="Riegel exponent"').vm.modelValue).to.equal(1.06); + await wrapper.findComponent('[aria-label="Riegel exponent"').setValue(1); + + // Calculate result + result = calculateResult({ distanceValue: 10, distanceUnit: 'kilometers', - time: prediction, result: 'time', }); + + // Assert result is correct + expect(result.time).to.equal(2400); }); -test('should correctly predict race distances', async () => { +test('should load input pace from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.race-calculator-input-distance', '1'); + localStorage.setItem('running-tools.race-calculator-input-unit', '"miles"'); + localStorage.setItem('running-tools.race-calculator-input-time', '600'); + // Initialize component const wrapper = shallowMount(RaceCalculator); - // Override input values - await wrapper.setData({ - inputDistance: 5, - inputUnit: 'kilometers', - inputTime: 1200, - }); + // Assert data loaded + expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1); + expect(wrapper.find('select[aria-label="Input distance unit"]').element.value).to.equal('miles'); + expect(wrapper.findComponent({ name: 'time-input' }).vm.modelValue).to.equal(600); +}); - // Predict race distances - const result = wrapper.vm.predictResult({ - time: 2460, - result: 'distance', +test('should save input pace to localStorage', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Enter input pace data + await wrapper.findComponent({ name: 'decimal-input' }).setValue(1); + await wrapper.find('select[aria-label="Input distance unit"]').setValue('miles'); + await wrapper.findComponent({ name: 'time-input' }).setValue(600); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.race-calculator-input-distance')).to.equal('1'); + expect(localStorage.getItem('running-tools.race-calculator-input-unit')).to.equal('"miles"'); + expect(localStorage.getItem('running-tools.race-calculator-input-time')).to.equal('600'); +}); + +test('should load selected target set from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.race-calculator-target-set', '"_pace_targets"'); + + // Initialize component + const paceTargets = [ + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 6, distanceUnit: 'kilometers' }, + ]; + const wrapper = shallowMount(RaceCalculator, { + data() { + return { + targetSets: { + '_pace_targets': { + name: 'Common pace targets', + targets: paceTargets, + }, + '_race_targets': { + name: 'Common race targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + }, + }; + }, }); - // Assert result is correct - const prediction = raceUtils.AverageModel.predictDistance(1200, 5000, 2460); - expect(result).to.deep.equal({ - distanceValue: unitUtils.convertDistance(prediction, 'meters', - unitUtils.getDefaultDistanceUnit(unitUtils.detectDefaultUnitSystem())), - distanceUnit: unitUtils.getDefaultDistanceUnit(unitUtils.detectDefaultUnitSystem()), - time: 2460, - result: 'distance', + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('_pace_targets'); + expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(paceTargets); +}); + +test('should save selected target set to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Select a new target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_pace_targets'); + + // New selected target set should be saved to localStorage + expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal('"_pace_targets"'); +}); + +test('should save default units setting to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator, { + data() { + return { + defaultUnitSystem: 'metric', + }; + }, }); + + // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); + + // New default units should be saved to localStorage + expect(localStorage.getItem('running-tools.default-unit-system')).to.equal('"imperial"'); }); + +test('should load advanced model options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.race-calculator-model', '"PurdyPointsModel"'); + localStorage.setItem('running-tools.race-calculator-riegel-exponent', '1.20'); + + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Assert data loaded + expect(wrapper.find('select[aria-label="Prediction model"]').element.value).to.equal('PurdyPointsModel'); + expect(wrapper.findComponent('[aria-label="Riegel exponent"]').vm.modelValue).to.equal(1.20); +}); + +test('should save advanced model options to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(RaceCalculator); + + // Update advanced model options + await wrapper.find('select[aria-label="Prediction model"]').setValue('CameronModel'); + await wrapper.findComponent('[aria-label="Riegel exponent"]').setValue(1.30); + + // Assert data saved to localStorage + expect(localStorage.getItem('running-tools.race-calculator-model')).to.equal('"CameronModel"'); + expect(localStorage.getItem('running-tools.race-calculator-riegel-exponent')).to.equal('1.3'); +}); + diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -1,11 +1,305 @@ -/* eslint-disable no-underscore-dangle */ - -import { test, expect } from 'vitest'; +import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import SplitCalculator from '@/views/SplitCalculator.vue'; -import unitUtils from '@/utils/units'; -test('should correctly calculate split paces and total times', async () => { +beforeEach(() => { + localStorage.clear(); +}) + +test('should initialize undefined splits to 0:00.00', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters' }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // 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 / km'); + 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 / km'); + 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 / km'); + 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(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // 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('3:00 / km'); + 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('3:10 / km'); + 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('3:20 / km'); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly handle null target set', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + 'B': null, + }, + selectedTargetSet: 'B', + defaultUnitSystem: 'metric', + }; + }, + }); + + // 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); + + // Switch to valid target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_split_targets'); + + // Assert results are correct + 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('3:00 / km'); + 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('3:10 / km'); + 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('3:20 / km'); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly calculate paces and cululative times from entered split times', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // Update split times + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(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')[3].element.textContent).to.equal('3:00 / km'); + 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('3:10 / km'); + 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('3:20 / km'); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should correctly sort split targets', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // 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 / km'); + 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 / km'); + 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 / km'); + expect(rows[2].findAll('td').length).to.equal(4); + expect(rows.length).to.equal(3); +}); + +test('should ignore time based targets', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, + { result: 'distance', time: 600 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters' }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // 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 / km'); + 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 / km'); + 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 / km'); + 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 component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // Update split times + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(200); + + // Assert targets correctly saved in localStorage + expect(localStorage.getItem('running-tools.target-sets')).to.equal(JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + })); +}); + +test('should update results when a new target set is selected', async () => { // Initialize component const wrapper = shallowMount(SplitCalculator, { data() { @@ -14,24 +308,200 @@ test('should correctly calculate split paces and total times', async () => { '_split_targets': { name: 'Split targets', targets: [ - { result: 'time', distanceValue: 2, distanceUnit: 'miles', split: 60 }, - { result: 'time', distanceValue: 4, distanceUnit: 'miles', split: 70 }, - { result: 'time', distanceValue: 10, distanceUnit: 'kilometers', split: 80 }, + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + 'B': { + name: 'Split targets #2', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, ], }, }, + defaultUnitSystem: 'metric', }; }, }); + // Assert default split targets are initially loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).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'); + // Assert results are correct - const final_split_distance = 10000 - unitUtils.convertDistance(4, 'miles', 'meters'); - const final_split_pace = unitUtils.convertPace(80 / final_split_distance, 'seconds_per_meter', - 'seconds_per_mile') - expect(wrapper.vm.results[0].totalTime).to.equal(60); - expect(wrapper.vm.results[0].pace).to.equal(30); - expect(wrapper.vm.results[1].totalTime).to.equal(130); - expect(wrapper.vm.results[1].pace).to.equal(35); - expect(wrapper.vm.results[2].totalTime).to.equal(210); - expect(final_split_pace - wrapper.vm.results[2].pace).to.lessThan(0.00001); + 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('3:00 / km'); + 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('3:10 / km'); + 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('3:20 / km'); + 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"'); + + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + 'B': { + name: 'Split targets #2', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // Assert selection is loaded + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).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('3:00 / km'); + 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('3:10 / km'); + 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('3:20 / km'); + 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, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + 'B': { + name: 'Split targets #2', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // Select a new target set + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('B'); + + // New selected target set should be saved to localStorage + expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal('"B"'); +}); + +test('should update paces according to default units setting', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles', split: 300 }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles', split: 300 }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers', split: 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.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'); +}); + +test('should save default units setting to localStorage when modified', async () => { + // Initialize component + const wrapper = shallowMount(SplitCalculator, { + data() { + return { + targetSets: { + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles', split: 300 }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles', split: 300 }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers', split: 330 }, + ], + }, + }, + defaultUnitSystem: 'metric', + }; + }, + }); + + // Change default units + 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"'); }); diff --git a/tests/unit/views/UnitCalculator.spec.js b/tests/unit/views/UnitCalculator.spec.js @@ -1,8 +1,5 @@ -/* eslint-disable no-underscore-dangle */ - import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; -import unitUtils from '@/utils/units'; import UnitCalculator from '@/views/UnitCalculator.vue'; beforeEach(() => {