running-tools

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

commit e3e104dfe68a7d79f7e4270755461505584ea960
parent bfa15cc01632840922978d1d3f79cfcee78417ef
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Mon,  9 Aug 2021 11:58:17 -0700

Refactor IntInput component

Diffstat:
Msrc/components/IntInput.vue | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtests/unit/IntInput.spec.js | 149++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 251 insertions(+), 62 deletions(-)

diff --git a/src/components/IntInput.vue b/src/components/IntInput.vue @@ -1,10 +1,16 @@ <template> - <input @keydown="keydown" @keypress="keypress" v-model="stringValue"> + <input + ref="input" + @blur="onblur" + @keydown="onkeydown" + @keypress="onkeypress" + v-model="stringValue"> </template> <script> export default { name: 'IntInput', + props: { /** * The input value @@ -19,10 +25,7 @@ export default { */ min: { type: Number, - default: 0, - validator: function(value) { - return value >= 0; - } + default: null, }, /** @@ -31,9 +34,6 @@ export default { max: { type: Number, default: null, - validator: function(value) { - return value >= 0; - } }, }, @@ -42,88 +42,146 @@ export default { /** * The internal value */ - intValue: this.value, - - /** - * The value of the input - */ - stringValue: this.value.toString(), + internalValue: this.value.toString(), }; }, - watch: { + computed: { /** - * Update the internal value from the value prop + * The value of the input element */ - value: function(newValue) { - this.stringValue = newValue; + stringValue: { + get: function() { + return this.internalValue; + }, + set: function(newValue) { + // Parse new value + let parsedValue = this.parse(newValue); + + // Allow input to be '' or '-' + if (newValue === '' || newValue === '-') { + this.internalValue = newValue; + } + + // Enforce minimum + else if (this.min !== null && parsedValue < this.min) { + this.internalValue = this.min.toString(); + } + + // Enforce maximum + else if (this.max !== null && parsedValue > this.max) { + this.internalValue = this.max.toString(); + } + + // Allow valid numbers + else if (!isNaN(parsedValue)) { + this.internalValue = newValue; + } + + // Make sure input element is updated + if (this.$refs.input.value === newValue) { + // Setter was called by the input element + if (this.internalValue !== newValue) { + // The value was corrected, so the input element must be updated + this.$refs.input.value = this.internalValue; + } + } + }, }, /** - * Trigger the input event + * The value of the component */ - intValue: function(newValue) { - this.$emit('input', newValue); + intValue: { + get: function() { + let parsedValue = parseInt(this.stringValue); + return isNaN(parsedValue) ? this.defaultValue : parsedValue; + }, + set: function(newValue) { + this.stringValue = newValue.toString(); + } }, /** - * Validate the new value + * The default value of the component */ - stringValue: function(newValue, oldValue) { - // Parse new value - let parsedValue = parseInt(newValue); - - // Make sure value is a number - if (isNaN(parsedValue)) { - if (newValue === '') { - parsedValue = this.min; - } - else { - parsedValue = this.intValue; - } + defaultValue: function() { + if (0 < this.min || 0 > this.max) { + return this.min; } - - // Enforce minimum and maximum - else if (this.min !== null && parsedValue < this.min) { - parsedValue = this.min; - } - else if (this.max !== null && parsedValue > this.max) { - parsedValue = this.max; + else { + return 0; } + } + }, - // Update and format string value - if (newValue !== parsedValue.toString()) { - this.stringValue = parsedValue.toString(); - } + watch: { + /** + * Update the component value when the value prop changes + * @param {Number} newValue The new prop value + */ + value: function(newValue) { + this.intValue = newValue; + }, - // Update intValue - this.intValue = parsedValue; + /** + * Emit the input event when the component value changes + * @param {Number} newValue The new component value + */ + intValue: function(newValue) { + this.$emit('input', newValue); }, }, methods: { /** * Restrict input to numbers + * @param {Object} e The keypress event args */ - keypress: function(e) { - if (!['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) { - /* key press was not a number */ + onkeypress: function(e) { + let validKeys = ['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + if (!validKeys.includes(e.key)) { + /* key was not a number */ e.preventDefault(); } }, /** * Process up and down arrow presses + * @param {Object} e The keydown event args */ - keydown: function(e) { + onkeydown: function(e) { if (e.key === 'ArrowUp') { - this.stringValue = (parseInt(this.stringValue) + 1).toString(); + this.intValue++; e.preventDefault(); } else if (e.key === 'ArrowDown') { - this.stringValue = (parseInt(this.stringValue) - 1).toString(); + this.intValue--; e.preventDefault(); } + }, + + /** + * Reformat display value + * @param {Object} e The blur event args + */ + onblur: function(e) { + this.stringValue = this.intValue.toString(); + }, + + /** + * Parse an integer from a string + * @param {String} value The string + * @returns {Number} The parsed integer + */ + parse: function(value) { + if (value.includes('.')) { + // value cannot be parsed as an integer + return NaN; + } + else { + return Number(value); + } } }, } diff --git a/tests/unit/IntInput.spec.js b/tests/unit/IntInput.spec.js @@ -4,93 +4,224 @@ import IntInput from '@/components/IntInput.vue'; describe('IntInput.vue', () => { it('value should be 0 by default', () => { + // Initialize component const wrapper = mount(IntInput); + + // Assert value is 0 expect(wrapper.find('input').element.value).to.equal('0'); }); it('should read value prop', () => { + // Initialize component const wrapper = mount(IntInput, { propsData: { value: 1 } }); + + // Assert value is 1 expect(wrapper.find('input').element.value).to.equal('1'); }); it('up arrow should increment value', async () => { + // Initialize component const wrapper = mount(IntInput); + + // Press up arrow await wrapper.trigger('keydown', { key: 'ArrowUp' }); + + // Assert value is 1 and input event was emitted expect(wrapper.find('input').element.value).to.equal('1'); expect(wrapper.emitted().input).to.deep.equal([[1]]); }); it('down arrow should increment value', async () => { - const wrapper = mount(IntInput, { - propsData: { value: 2 } - }); + // Initialize component + const wrapper = mount(IntInput); + + // Press down arrow await wrapper.trigger('keydown', { key: 'ArrowDown' }); - expect(wrapper.find('input').element.value).to.equal('1'); - expect(wrapper.emitted().input).to.deep.equal([[1]]); + + // Assert value is -1 and input event was emitted + expect(wrapper.find('input').element.value).to.equal('-1'); + expect(wrapper.emitted().input).to.deep.equal([[-1]]); }); it('should fire input event when value changes', async () => { + // Initialize component const wrapper = mount(IntInput); - await wrapper.trigger('keydown', { key: 'ArrowUp' }); + + // Set value to 1 + wrapper.find('input').element.value = '1'; + await wrapper.find('input').trigger('input'); + + // Assert input event was emitted expect(wrapper.emitted().input).to.deep.equal([[1]]); }); it('should accept numerical values', async () => { + // Initialize component const wrapper = mount(IntInput); + + // Try to set value to 1 wrapper.find('input').element.value = '1'; await wrapper.find('input').trigger('input'); + + // Assert value was accepted and input event was emitted expect(wrapper.find('input').element.value).to.equal('1'); expect(wrapper.emitted().input).to.deep.equal([[1]]); }); + it('should not accept decimal values', async () => { + // Initialize component + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + + // Try to set value to 1.5 + wrapper.find('input').element.value = '1.5'; + await wrapper.find('input').trigger('input'); + + // Assert value was not accepted and no events were emitted + expect(wrapper.find('input').element.value).to.equal('1'); + expect(wrapper.emitted().input).to.be.undefined; + }); + it('should not accept non numerical values', async () => { + // Initialize component const wrapper = mount(IntInput, { propsData: { value: 1 } }); + + // Try to set value to a wrapper.find('input').element.value = 'a'; await wrapper.find('input').trigger('input'); + + // Assert value was not accepted and no events were emitted expect(wrapper.find('input').element.value).to.equal('1'); expect(wrapper.emitted().input).to.be.undefined; }); - it('should remove leading zeros', async () => { + it('should format input value on blur', async () => { + // Initialize component const wrapper = mount(IntInput, { propsData: { value: 1 } }); + + // Set value to '01' wrapper.find('input').element.value = '01'; await wrapper.find('input').trigger('input'); + + // Assert value was not updated and no events were emitted + expect(wrapper.find('input').element.value).to.equal('01'); + expect(wrapper.emitted().input).to.be.undefined; + + // Trigger blur event + await wrapper.find('input').trigger('blur'); + + // Assert value was formatted but no events were emitted expect(wrapper.find('input').element.value).to.equal('1'); expect(wrapper.emitted().input).to.be.undefined; }); - it('should set empty input to minimum', async () => { + it('should allow input to be empty until blur', async () => { + // Initialize component + const wrapper = mount(IntInput, { + propsData: { value: 5 } + }); + + // Set value to '' + wrapper.find('input').element.value = ''; + await wrapper.find('input').trigger('input'); + + // Assert value is '' and input event was emitted with default value + expect(wrapper.find('input').element.value).to.equal(''); + expect(wrapper.emitted().input).to.deep.equal([[0]]); + + // Trigger blur event + await wrapper.find('input').trigger('blur'); + + // Assert value is the default value but no new events were emitted + expect(wrapper.find('input').element.value).to.equal('0'); + expect(wrapper.emitted().input).to.deep.equal([[0]]); + }); + + it('should allow input to be "-" until blur', async () => { + // Initialize component + const wrapper = mount(IntInput, { + propsData: { value: 5 } + }); + + // Set value to '-' + wrapper.find('input').element.value = '-'; + await wrapper.find('input').trigger('input'); + + // Assert value is '-' and input event was emitted with default value + expect(wrapper.find('input').element.value).to.equal('-'); + expect(wrapper.emitted().input).to.deep.equal([[0]]); + + // Trigger blur event + await wrapper.find('input').trigger('blur'); + + // Assert value is the default value but no new events were emitted + expect(wrapper.find('input').element.value).to.equal('0'); + expect(wrapper.emitted().input).to.deep.equal([[0]]); + }); + + it('default value should be the minimum if 0 is not valid', async () => { + // Initialize component const wrapper = mount(IntInput, { - propsData: { value: 5, min: 2 } + propsData: { value: 3, max: 4, min: 2 } }); + + // Set value to '' and trigger blur event so value must be updated wrapper.find('input').element.value = ''; await wrapper.find('input').trigger('input'); + await wrapper.find('input').trigger('blur'); + + // Assert value is 2 and input event was emitted expect(wrapper.find('input').element.value).to.equal('2'); expect(wrapper.emitted().input).to.deep.equal([[2]]); }); it('should not allow input to be below the minimum', async () => { + // Initialize component const wrapper = mount(IntInput, { propsData: { min: 10, value: 20 } }); + + // Try to set value to 9, which is below the minimum wrapper.find('input').element.value = '9'; await wrapper.find('input').trigger('input'); + + // Assert value is 10 and input event was emitted + expect(wrapper.find('input').element.value).to.equal('10'); + expect(wrapper.emitted().input).to.deep.equal([[10]]); + + // Try to decrement value + await wrapper.trigger('keydown', { key: 'ArrowDown' }); + + // Assert value is still 10 and no new event were emitted expect(wrapper.find('input').element.value).to.equal('10'); expect(wrapper.emitted().input).to.deep.equal([[10]]); }); it('should not allow input to be above the maximum', async () => { + // Initialize component const wrapper = mount(IntInput, { propsData: { max: 10 } }); + + // Try to set value to 11, which is above the maximum wrapper.find('input').element.value = '11'; await wrapper.find('input').trigger('input'); + + // Assert value is 10 and input event was emitted + expect(wrapper.find('input').element.value).to.equal('10'); + expect(wrapper.emitted().input).to.deep.equal([[10]]); + + // Try to increment value + await wrapper.trigger('keydown', { key: 'ArrowUp' }); + + // Assert value is still 10 and no new events were emitted expect(wrapper.find('input').element.value).to.equal('10'); expect(wrapper.emitted().input).to.deep.equal([[10]]); });