running-tools

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

commit c32c593600175dfa07e5a378185ce390eeaa38b8
parent 6e641355ca8b5fd06e5639850709e80fa14bf4a5
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Mon,  2 Aug 2021 21:50:13 -0700

Implement IntInput component

Diffstat:
Asrc/components/IntInput.vue | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/IntInput.spec.js | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+), 0 deletions(-)

diff --git a/src/components/IntInput.vue b/src/components/IntInput.vue @@ -0,0 +1,128 @@ +<template> + <input @keydown="keydown" @keypress="keypress" v-model="stringValue"> +</template> + +<script> +export default { + name: 'IntInput', + props: { + /** + * The input value + */ + value: { + type: Number, + default: 0, + }, + + /** + * The minimum value + */ + min: { + type: Number, + default: 0, + }, + + /** + * The maximum value + */ + max: { + type: Number, + default: null, + }, + }, + + data: function() { + return { + /** + * The internal value + */ + intValue: this.value, + + /** + * The value of the input + */ + stringValue: this.value.toString(), + }; + }, + + watch: { + /** + * Update the internal value from the value prop + */ + value: function(newValue) { + this.stringValue = newValue; + }, + + /** + * Trigger the input event + */ + intValue: function(newValue) { + this.$emit('input', newValue); + }, + + /** + * Validate the new value + */ + stringValue: function(newValue, oldValue) { + // Parse new value + let parsedValue = parseInt(newValue); + + // Make sure value is a number + if (isNaN(parsedValue)) { + if (newValue === '') { + parsedValue = 0; + this.stringValue = '0'; + } + else { + parsedValue = this.intValue; + this.stringValue = oldValue; + } + } + + // Enforce minimum and maximum + else if (this.min !== null && parsedValue < this.min) { + parsedValue = this.min; + this.stringValue = this.min.toString(); + } + else if (this.max !== null && parsedValue > this.max) { + parsedValue = this.max; + this.stringValue = this.max.toString(); + } + + // Make sure new value is correctly formatted + else if (newValue !== parsedValue.toString()) { + this.stringValue = parsedValue.toString(); + } + + // Update intValue + this.intValue = parsedValue; + }, + }, + + methods: { + /** + * Restrict input to numbers + */ + keypress: function(e) { + if (!['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) { + /* key press was not a number */ + e.preventDefault(); + } + }, + + /** + * Process up and down arrow presses + */ + keydown: function(e) { + if (e.key === 'ArrowUp') { + this.stringValue = (parseInt(this.stringValue) + 1).toString(); + e.preventDefault(); + } + else if (e.key === 'ArrowDown') { + this.stringValue = (parseInt(this.stringValue) - 1).toString(); + e.preventDefault(); + } + } + }, +} +</script> diff --git a/tests/unit/IntInput.spec.js b/tests/unit/IntInput.spec.js @@ -0,0 +1,106 @@ +import { expect } from 'chai'; +import { mount } from '@vue/test-utils'; +import IntInput from '@/components/IntInput.vue'; + +describe('IntInput.vue', () => { + it('value should be 0 by default', () => { + const wrapper = mount(IntInput); + expect(wrapper.find('input').element.value).to.equal('0'); + }); + + it('should read value prop', () => { + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + expect(wrapper.find('input').element.value).to.equal('1'); + }); + + it('should update when value prop changes', () => { + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + wrapper.props().value = 2; + expect(wrapper.find('input').element.value).to.equal('2'); + expect(wrapper.emitted().input).to.deep.equal([[2]]); + }); + + it('up arrow should increment value', async () => { + const wrapper = mount(IntInput); + await wrapper.trigger('keydown', { key: 'ArrowUp' }); + 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 } + }); + await wrapper.trigger('keydown', { key: 'ArrowDown' }); + 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 () => { + const wrapper = mount(IntInput); + await wrapper.trigger('keydown', { key: 'ArrowUp' }); + expect(wrapper.emitted().input).to.deep.equal([[1]]); + }); + + it('should accept numerical values', async () => { + const wrapper = mount(IntInput); + wrapper.find('input').element.value = '1'; + await wrapper.find('input').trigger('input'); + expect(wrapper.find('input').element.value).to.equal('1'); + expect(wrapper.emitted().input).to.deep.equal([[1]]); + }); + + it('should not accept non numerical values', async () => { + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + wrapper.find('input').element.value = 'a'; + await wrapper.find('input').trigger('input'); + expect(wrapper.find('input').element.value).to.equal('1'); + expect(wrapper.emitted().input).to.be.undefined; + }); + + it('should remove leading zeros', async () => { + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + wrapper.find('input').element.value = '01'; + await wrapper.find('input').trigger('input'); + expect(wrapper.find('input').element.value).to.equal('1'); + expect(wrapper.emitted().input).to.be.undefined; + }); + + it('should set empty input to 0', async () => { + const wrapper = mount(IntInput, { + propsData: { value: 1 } + }); + wrapper.find('input').element.value = ''; + await wrapper.find('input').trigger('input'); + expect(wrapper.find('input').element.value).to.equal('0'); + expect(wrapper.emitted().input).to.deep.equal([[0]]); + }); + + it('should not allow input to be below the minimum', async () => { + const wrapper = mount(IntInput, { + propsData: { min: 10, value: 20 } + }); + wrapper.find('input').element.value = '9'; + await wrapper.find('input').trigger('input'); + 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 () => { + const wrapper = mount(IntInput, { + propsData: { max: 10 } + }); + wrapper.find('input').element.value = '11'; + await wrapper.find('input').trigger('input'); + expect(wrapper.find('input').element.value).to.equal('10'); + expect(wrapper.emitted().input).to.deep.equal([[10]]); + }); +});