From fb265046ad1b05575d47e7f48beffe44c8ec8182 Mon Sep 17 00:00:00 2001 From: Asher Morgan <59518073+ashermorgan@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:57:48 -0700 Subject: [PATCH] Add advanced options section to Batch Calculator --- src/views/BatchCalculator.vue | 61 ++++++++-- tests/e2e/batch-calculator.spec.js | 102 ++++++++++++++++- tests/e2e/cross-calculator.spec.js | 38 +++--- tests/unit/views/BatchCalculator.spec.js | 140 +++++++++++++++++++++++ 4 files changed, 311 insertions(+), 30 deletions(-) diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue index 276b0ed..d597d6d 100644 --- a/src/views/BatchCalculator.vue +++ b/src/views/BatchCalculator.vue @@ -23,6 +23,25 @@ +
+ +

Advanced Options

+
+
+ Default units: + +
+
+ Target Set: + +
+ +
+

Batch Results

{ /** * The selected target set for the current calculator */ -const selectedTargetSet = computed(() => { - if (options.value.calculator === 'pace') { - return selectedPaceTargetSet.value; - } else if (options.value.calculator === 'race') { - return selectedRaceTargetSet.value; - } else { - return selectedWorkoutTargetSet.value; - } +const selectedTargetSet = computed({ + get: () => { + if (options.value.calculator === 'pace') { + return selectedPaceTargetSet.value; + } else if (options.value.calculator === 'race') { + return selectedRaceTargetSet.value; + } else { + return selectedWorkoutTargetSet.value; + } + }, + set: (newValue) => { + if (options.value.calculator === 'pace') { + selectedPaceTargetSet.value = newValue; + } else if (options.value.calculator === 'race') { + selectedRaceTargetSet.value = newValue; + } else { + selectedWorkoutTargetSet.value = newValue; + } + }, }); /** @@ -144,6 +176,19 @@ const targetSets = computed(() => { } }); +/** + * The advanced options for the current calculator + */ +const advancedOptions = computed(() => { + if (options.value.calculator === 'pace') { + return {}; + } else if (options.value.calculator === 'race') { + return raceOptions.value; + } else { + return workoutOptions.value; + } +}); + /** * The appropriate calculate_results function for the current calculator */ diff --git a/tests/e2e/batch-calculator.spec.js b/tests/e2e/batch-calculator.spec.js index 33c8d6b..231bee5 100644 --- a/tests/e2e/batch-calculator.spec.js +++ b/tests/e2e/batch-calculator.spec.js @@ -31,6 +31,22 @@ test('Basic usage', async ({ page }) => { await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); await expect(page.getByRole('row')).toHaveCount(16); + // Change prediction model + await page.getByText('Advanced Options').click(); + await page.getByLabel('Prediction model').selectOption('Riegel\'s Model'); + + // Assert workout results are correct + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km'); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:40.78'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:16.51'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row')).toHaveCount(16); + // Change calculator await expect(page.getByLabel('Calculator')).toHaveValue('workout'); await page.getByLabel('Calculator').selectOption('Pace Calculator'); @@ -50,6 +66,27 @@ test('Basic usage', async ({ page }) => { await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(31); await expect(page.getByRole('row')).toHaveCount(16); + // Assert prediction options are hidden + await expect(page.getByLabel('Prediction model')).toHaveCount(0); + + // Change default units + await page.getByLabel('Default units').selectOption('Kilometers'); + + // Assert pace results are correct + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(6)).toHaveText('800 m'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(28)).toHaveText('10:00'); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(31); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('2:36.58'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(28)).toHaveText('3.07 km'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(31); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(6)).toHaveText('3:11.38'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(28)).toHaveText('2.51 km'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(31); + await expect(page.getByRole('row')).toHaveCount(16); + // Change calculator await page.getByLabel('Calculator').selectOption('Race Calculator'); @@ -64,6 +101,22 @@ test('Basic usage', async ({ page }) => { await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:43.61'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); + + // Change Riegel exponent + await expect(page.getByLabel('Prediction model')).toHaveValue('AverageModel'); + await page.getByLabel('Riegel Exponent').fill('1.12'); + + // Assert race results are correct + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:11.72'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:40.09'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row')).toHaveCount(16); }); test('Save settings across page reloads', async ({ page }) => { @@ -83,24 +136,67 @@ test('Save settings across page reloads', async ({ page }) => { await page.getByLabel('Duration increment seconds').fill('10'); await page.getByLabel('Number of rows').fill('15'); + // Change workout prediction model + await page.getByText('Advanced Options').click(); + await page.getByLabel('Prediction model').selectOption('Riegel\'s Model'); + // Change calculator await page.getByLabel('Calculator').selectOption('Pace Calculator'); + // Change default units + await page.getByLabel('Default units').selectOption('Kilometers'); + + // Change calculator + await page.getByLabel('Calculator').selectOption('Race Calculator'); + + // Change Riegel exponent + await page.getByLabel('Riegel Exponent').fill('1.12'); + // Reload page await page.reload(); - // Assert pace results are correct (inputs and options not reset) + // Assert race results are correct (inputs and options not reset) + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:11.72'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:40.09'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row')).toHaveCount(16); + + // Change calculator + await page.getByLabel('Calculator').selectOption('Workout Calculator'); + + // Assert workout results are correct + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); + await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m @ 5 km'); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:40.78'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:16.51'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(5); + await expect(page.getByRole('row')).toHaveCount(16); + + // Change calculator + await page.getByLabel('Calculator').selectOption('Pace Calculator'); + + // Assert pace results are correct await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(6)).toHaveText('800 m'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(28)).toHaveText('10:00'); await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(31); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('2:36.58'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(28)).toHaveText('1.90 mi'); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(28)).toHaveText('3.07 km'); await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(31); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(6)).toHaveText('3:11.38'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(28)).toHaveText('1.56 mi'); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(28)).toHaveText('2.51 km'); await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(31); await expect(page.getByRole('row')).toHaveCount(16); }); diff --git a/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js index 8f64687..afe7359 100644 --- a/tests/e2e/cross-calculator.spec.js +++ b/tests/e2e/cross-calculator.spec.js @@ -18,7 +18,11 @@ test('Save and update state when navigating between calculators', async ({ page await page.getByLabel('Number of rows').fill('15'); // Change calculator - await page.getByLabel('Calculator').selectOption('Pace Calculator'); + await page.getByLabel('Calculator').selectOption('Race Calculator'); + + // Change prediction model + await page.getByText('Advanced Options').click(); + await page.getByLabel('Prediction model').selectOption('Riegel\'s Model'); // Go to pace calculator await page.goto('/'); @@ -61,10 +65,6 @@ test('Save and update state when navigating between calculators', async ({ page await page.getByLabel('Input race duration minutes').fill('10'); await page.getByLabel('Input race duration seconds').fill('30'); - // Change prediction model - await page.getByText('Advanced Options').click(); - await page.getByLabel('Prediction model').selectOption('Riegel\'s Model'); - // Go to split calculator await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Split Calculator' }).click(); @@ -118,29 +118,29 @@ test('Save and update state when navigating between calculators', async ({ page await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Batch Calculator' }).click(); - // Assert pace results are correct (inputs and options not reset, new pace targets loaded) + // Assert pace results are correct (inputs and options not reset) await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); - await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(4); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:36.58'); - await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(4); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24.04'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:11.38'); - await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56.05'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); await expect(page.getByRole('row')).toHaveCount(16); - // Assert race results are correct (new race options loaded) - await page.getByLabel('Calculator').selectOption('Race Calculator'); + // Assert pace results are correct (inputs and options not reset, new pace targets loaded) + await page.getByLabel('Calculator').selectOption('Pace Calculator'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(0)).toHaveText('2 mi'); await expect(page.getByRole('row').nth(0).getByRole('cell').nth(2)).toHaveText('800 m'); - await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(0).getByRole('cell')).toHaveCount(4); await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toHaveText('10:30'); - await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:24.04'); - await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('2:36.58'); + await expect(page.getByRole('row').nth(1).getByRole('cell')).toHaveCount(4); await expect(page.getByRole('row').nth(15).getByRole('cell').nth(0)).toHaveText('12:50'); - await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('2:56.05'); - await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(17); + await expect(page.getByRole('row').nth(15).getByRole('cell').nth(2)).toHaveText('3:11.38'); + await expect(page.getByRole('row').nth(15).getByRole('cell')).toHaveCount(4); await expect(page.getByRole('row')).toHaveCount(16); // Assert workout results are correct (new workout options loaded) @@ -170,7 +170,7 @@ test('Save and update state when navigating between calculators', async ({ page await page.getByRole('link', { name: 'Back' }).click(); await page.getByRole('button', { name: 'Race Calculator' }).click(); - // Assert race predictions are correct (input race and prediction model not reset) + // Assert race predictions are correct (input race not resset and new prediction model loaded) await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km'); await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.86' + '3:21 / km'); await expect(page.getByRole('row')).toHaveCount(17); diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js index ef89670..360a286 100644 --- a/tests/unit/views/BatchCalculator.spec.js +++ b/tests/unit/views/BatchCalculator.spec.js @@ -96,6 +96,33 @@ test('should save batch options to localStorage when modified', async () => { })); }); +test('should load default units setting from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.default-unit-system', '"metric"'); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Assert default units setting loaded + expect(wrapper.find('select[aria-label="Default units"]').element.value).to.equal('metric'); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.defaultUnitSystem) + .to.equal('metric'); +}); + +test('should save default units setting from localStorage when modified', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.default-unit-system', '"metric"'); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Change default units setting + 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 selected target set from localStorage', async () => { // Initialize localStorage const selectedTargetSets = [ @@ -157,20 +184,133 @@ test('should load selected target set from localStorage', async () => { // Assert selected pace target set is loaded await wrapper.find('select[aria-label="Calculator"]').setValue('pace'); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('A'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[0].targets); // Assert selected race target set is loaded await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('C'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[1].targets); // Assert selected workout target set is loaded await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); + expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.selectedTargetSet).to.equal('E'); expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) .to.deep.equal(selectedTargetSets[2].targets); }); +test('should save selected target set to localStorage when modified', async () => { + // Initialize localStorage + const selectedTargetSets = [ + { + name: 'Pace targets #1', + targets: [ + { type: 'distance', distanceValue: 1, distanceUnit: 'miles' }, + ], + }, + { + name: 'Race targets #1', + targets: [ + { type: 'distance', distanceValue: 3, distanceUnit: 'miles' }, + ], + }, + { + name: 'Workout targets #1', + targets: [ + { + type: 'distance', distanceValue: 5, distanceUnit: 'miles', + splitValue: 1, splitUnit: 'miles' + }, + ], + }, + ]; + localStorage.setItem('running-tools.pace-calculator-target-sets', JSON.stringify({ + 'A': selectedTargetSets[0], + 'B': { + name: 'Pace targets #2', + targets: [ + { type: 'distance', distanceValue: 2, distanceUnit: 'miles' }, + ], + } + })); + localStorage.setItem('running-tools.pace-calculator-target-set', '"B"'); + localStorage.setItem('running-tools.race-calculator-target-sets', JSON.stringify({ + 'C': selectedTargetSets[1], + 'D': { + name: 'Race targets #2', + targets: [ + { type: 'distance', distanceValue: 4, distanceUnit: 'miles' }, + ], + } + })); + localStorage.setItem('running-tools.race-calculator-target-set', '"D"'); + localStorage.setItem('running-tools.workout-calculator-target-sets', JSON.stringify({ + 'E': selectedTargetSets[2], + 'F': { + name: 'Workout targets #2', + targets: [ + { type: 'distance', distanceValue: 6, distanceUnit: 'miles' }, + ], + } + })); + localStorage.setItem('running-tools.workout-calculator-target-set', '"F"'); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Update selected pace target set + await wrapper.find('select[aria-label="Calculator"]').setValue('pace'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('A', 'selectedTargetSet'); + expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) + .to.deep.equal(selectedTargetSets[0].targets); + expect(localStorage.getItem('running-tools.pace-calculator-target-set')).to.equal('"A"'); + + // Assert selected race target set is loaded + await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('C', 'selectedTargetSet'); + expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) + .to.deep.equal(selectedTargetSets[1].targets); + expect(localStorage.getItem('running-tools.race-calculator-target-set')).to.equal('"C"'); + + // Assert selected workout target set is loaded + await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('E', 'selectedTargetSet'); + expect(wrapper.findComponent({ name: 'double-output-table' }).vm.targets) + .to.deep.equal(selectedTargetSets[2].targets); + expect(localStorage.getItem('running-tools.workout-calculator-target-set')).to.equal('"E"'); +}); + +test('should load advanced model options from localStorage', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.race-calculator-options', JSON.stringify({ + model: 'PurdyPointsModel', + riegelExponent: 1.2, + })); + localStorage.setItem('running-tools.workout-calculator-options', JSON.stringify({ + model: 'RiegelModel', + riegelExponent: 1.1, + })); + + // Initialize component + const wrapper = shallowMount(BatchCalculator); + + // Assert race prediction options are loaded + await wrapper.find('select[aria-label="Calculator"]').setValue('race'); + expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({ + model: 'PurdyPointsModel', + riegelExponent: 1.2, + }); + + // Assert workout prediction options are loaded + await wrapper.find('select[aria-label="Calculator"]').setValue('workout'); + expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({ + model: 'RiegelModel', + riegelExponent: 1.1, + }); +}); + test('should pass correct input props to DoubleOutputTable', async () => { // Initialize component const wrapper = shallowMount(BatchCalculator);