running-tools

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

commit 01d69427eed244b3c3ff18627f39662902f6052e
parent 838fee3c1801df43c0415b1f90833890a37a9a7c
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Tue, 12 Mar 2024 19:08:49 -0700

Use native HTML validation in input componenets

Diffstat:
Msrc/assets/global.css | 18++++++++++++++++++
Msrc/components/DecimalInput.vue | 167++++++++++++-------------------------------------------------------------------
Msrc/components/IntegerInput.vue | 170++++++++++++-------------------------------------------------------------------
Msrc/components/TimeInput.vue | 15+++++++--------
Mtests/unit/components/DecimalInput.spec.js | 258++++++-------------------------------------------------------------------------
Mtests/unit/components/IntegerInput.spec.js | 230++++++-------------------------------------------------------------------------
6 files changed, 107 insertions(+), 751 deletions(-)

diff --git a/src/assets/global.css b/src/assets/global.css @@ -31,6 +31,18 @@ a:focus, .link:focus { } } +/* remove spinner from numeric inputs */ +input[type=number] { + -moz-appearance: textfield; + appearance: textfield; + margin: 0; +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + /* styles for tables */ table { border-collapse: collapse; @@ -110,6 +122,9 @@ dialog { a, .link { color: var(--link); } +input:invalid:not(:focus) { + border: 1px solid var(--error); +} /* light/default theme */ :root { @@ -136,6 +151,9 @@ a, .link { /* The color of links */ --link: hsl(210, 100%, 40%); + + /* The error color */ + --error: hsl(0, 100%, 40%); } /* dark mode */ diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue @@ -1,10 +1,5 @@ <template> - <input - ref="input" - @blur="onblur" - @keydown="onkeydown" - @keypress="onkeypress" - v-model="stringValue"> + <input ref="input" type="number" step="any" required @blur="onblur" v-model="stringValue"> </template> <script> @@ -23,38 +18,6 @@ export default { }, /** - * The minimum value - */ - min: { - type: Number, - default: null, - }, - - /** - * The maximum value - */ - max: { - type: Number, - default: null, - }, - - /** - * The step value - */ - step: { - type: Number, - default: 1, - }, - - /** - * Whether to allow the user to increment/decrement the value using the arrow keys - */ - arrowKeys: { - type: Boolean, - default: true, - }, - - /** * The number of digits to show before the decimal point */ padding: { @@ -80,71 +43,15 @@ export default { data() { return { /** - * The internal value + * The internal float value */ - internalValue: this.format(this.modelValue), - }; - }, + internalValue: this.modelValue, - computed: { - /** - * The value of the input element - */ - stringValue: { - get() { - return this.internalValue; - }, - set(newValue) { - // Parse new value - const parsedValue = this.parse(newValue); - - if (newValue === '' || newValue === '-' || newValue === '.') { - // Allow input to be '' or '-' or '.' - this.internalValue = newValue; - } else if (this.min !== null && parsedValue < this.min) { - // Enforce minimum - this.internalValue = this.format(this.min); - } else if (this.max !== null && parsedValue > this.max) { - // Enforce maximum - this.internalValue = this.format(this.max); - } else if (!Number.isNaN(parsedValue)) { - // Allow valid numbers - 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; - } - } - }, - }, - - /** - * The value of the component - */ - decValue: { - get() { - const parsedValue = parseFloat(this.stringValue); - return Number.isNaN(parsedValue) ? this.defaultValue : parsedValue; - }, - set(newValue) { - this.stringValue = this.format(newValue); - }, - }, - - /** - * The default value of the component - */ - defaultValue() { - if (this.min > 0 || this.max < 0) { - return this.min; - } - return 0; - }, + /** + * The raw string value (empty if input is currently invalid) + */ + stringValue: this.format(this.modelValue), + }; }, watch: { @@ -153,63 +60,39 @@ export default { * @param {Number} newValue The new prop value */ modelValue(newValue) { - if (newValue !== this.decValue) { - this.decValue = newValue; + if (newValue !== this.internalValue) { + this.internalValue = newValue; + this.stringValue = this.format(this.internalValue); } }, /** - * Emit the input event when the component value changes - * @param {Number} newValue The new component value + * Emit the input event when the internal value changes + * @param {Number} newValue The new internal float value */ - decValue(newValue) { + internalValue(newValue) { this.$emit('update:modelValue', newValue); }, - }, - - methods: { - /** - * Restrict input to valid keys - * @param {Object} e The keypress event args - */ - onkeypress(e) { - const valid = ['.', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - if (!valid.includes(e.key)) { - /* key was not valid */ - e.preventDefault(); - } - }, /** - * Process up and down arrow presses - * @param {Object} e The keydown event args + * Update the float value when the raw string value changes + * @param {Number} newValue The new raw string value */ - onkeydown(e) { - if (this.arrowKeys) { - if (e.key === 'ArrowUp') { - this.decValue += this.step; - e.preventDefault(); - } else if (e.key === 'ArrowDown') { - this.decValue -= this.step; - e.preventDefault(); - } + stringValue(newValue) { + if (this.$refs.input.validity.valid) { + this.internalValue = Number(newValue); } }, + }, + methods: { /** - * Reformat display value + * Reformat display value if not invalid */ onblur() { - this.stringValue = this.format(this.decValue); - }, - - /** - * Parse a decimal from a string - * @param {String} value The string - * @returns {Number} The parsed decimal - */ - parse(value) { - return Number(value); + if (this.$refs.input.validity.valid) { + this.stringValue = this.format(this.internalValue); + } }, /** diff --git a/src/components/IntegerInput.vue b/src/components/IntegerInput.vue @@ -1,10 +1,5 @@ <template> - <input - ref="input" - @blur="onblur" - @keydown="onkeydown" - @keypress="onkeypress" - v-model="stringValue"> + <input ref="input" type="number" step="1" required @blur="onblur" v-model="stringValue"> </template> <script> @@ -21,38 +16,6 @@ export default { }, /** - * The minimum value - */ - min: { - type: Number, - default: null, - }, - - /** - * The maximum value - */ - max: { - type: Number, - default: null, - }, - - /** - * The step value - */ - step: { - type: Number, - default: 1, - }, - - /** - * Whether to allow the user to increment/decrement the value using the arrow keys - */ - arrowKeys: { - type: Boolean, - default: true, - }, - - /** * The number of digits to show before the decimal point */ padding: { @@ -67,71 +30,15 @@ export default { data() { return { /** - * The internal value + * The internal integer value */ - internalValue: this.format(this.modelValue), - }; - }, - - computed: { - /** - * The value of the input element - */ - stringValue: { - get() { - return this.internalValue; - }, - set(newValue) { - // Parse new value - const parsedValue = this.parse(newValue); - - if (newValue === '' || newValue === '-') { - // Allow input to be '' or '-' - this.internalValue = newValue; - } else if (this.min !== null && parsedValue < this.min) { - // Enforce minimum - this.internalValue = this.format(this.min); - } else if (this.max !== null && parsedValue > this.max) { - // Enforce maximum - this.internalValue = this.format(this.max); - } else if (!Number.isNaN(parsedValue)) { - // Allow valid numbers - 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; - } - } - }, - }, - - /** - * The value of the component - */ - intValue: { - get() { - const parsedValue = parseInt(this.stringValue, 10); - return Number.isNaN(parsedValue) ? this.defaultValue : parsedValue; - }, - set(newValue) { - this.stringValue = this.format(newValue); - }, - }, + internalValue: this.modelValue, - /** - * The default value of the component - */ - defaultValue() { - if (this.min > 0 || this.max < 0) { - return this.min; - } - return 0; - }, + /** + * The raw string value (empty if input is currently invalid) + */ + stringValue: this.format(this.modelValue), + }; }, watch: { @@ -140,68 +47,39 @@ export default { * @param {Number} newValue The new prop value */ modelValue(newValue) { - if (newValue !== this.intValue) { - this.intValue = newValue; + if (newValue !== this.internalValue) { + this.internalValue = newValue; + this.stringValue = this.format(this.internalValue); } }, /** - * Emit the input event when the component value changes - * @param {Number} newValue The new component value + * Emit the input event when the internal value changes + * @param {Number} newValue The new internal integer value */ - intValue(newValue) { + internalValue(newValue) { this.$emit('update:modelValue', newValue); }, - }, - - methods: { - /** - * Restrict input to numbers - * @param {Object} e The keypress event args - */ - onkeypress(e) { - const 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 + * Update the integer value when the raw string value changes + * @param {Number} newValue The new raw string value */ - onkeydown(e) { - if (this.arrowKeys) { - if (e.key === 'ArrowUp') { - this.intValue += this.step; - e.preventDefault(); - } else if (e.key === 'ArrowDown') { - this.intValue -= this.step; - e.preventDefault(); - } + stringValue(newValue) { + if (this.$refs.input.validity.valid) { + this.internalValue = Number(newValue); } }, + }, + methods: { /** - * Reformat display value + * Reformat display value if not invalid */ onblur() { - this.stringValue = this.format(this.intValue); - }, - - /** - * Parse an integer from a string - * @param {String} value The string - * @returns {Number} The parsed integer - */ - parse(value) { - if (value.includes('.')) { - // value cannot be parsed as an integer - return NaN; + if (this.$refs.input.validity.valid) { + this.stringValue = this.format(this.internalValue); } - - return Number(value); }, /** diff --git a/src/components/TimeInput.vue b/src/components/TimeInput.vue @@ -1,16 +1,15 @@ <template> <div class="time-input"> <integer-input class="hours" :aria-label="label + ' hours'" v-if="showHours" - :min="0" :max="99" :padding="1" v-model="hours" - :arrow-keys="false" @keydown="onkeydown($event, 3600)"/> + :min="0" :max="99" :padding="1" v-model="hours" @keydown="onkeydown($event, 3600)"/> <span v-if="showHours">:</span> <integer-input class="minutes" :aria-label="label + ' minutes'" :min="0" :max="59" :padding="2" v-model="minutes" - :arrow-keys="false" @keydown="onkeydown($event, 60)"/> + @keydown="onkeydown($event, 60)"/> <span>:</span> <decimal-input class="seconds" :aria-label="label + ' seconds'" :min="0" :max="59.99" :padding="2" :digits="2" v-model="seconds" - :arrow-keys="false" @keydown="onkeydown($event, 1)"/> + @keydown="onkeydown($event, 1)"/> </div> </template> @@ -136,17 +135,17 @@ export default { */ onkeydown(e, step = 1) { if (e.key === 'ArrowUp') { - if (this.internalValue + step > this.max) { + if (Math.floor(this.internalValue) + step > this.max) { this.internalValue = this.max; } else { - this.internalValue += step; + this.internalValue = Math.floor(this.internalValue) + step; } e.preventDefault(); } else if (e.key === 'ArrowDown') { - if (this.internalValue - step < 0) { + if (Math.ceil(this.internalValue) - step < 0) { this.internalValue = 0; } else { - this.internalValue -= step; + this.internalValue = Math.ceil(this.internalValue) - step; } e.preventDefault(); } diff --git a/tests/unit/components/DecimalInput.spec.js b/tests/unit/components/DecimalInput.spec.js @@ -2,274 +2,52 @@ import { test, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import DecimalInput from '@/components/DecimalInput.vue'; -test('value should be 0.0 by default', () => { - // Initialize component - const wrapper = mount(DecimalInput); - - // Assert value is 0.0 - expect(wrapper.find('input').element.value).to.equal('0.0'); -}); - -test('should read value prop', () => { +test('should be initialized to modelValue', () => { // Initialize component const wrapper = mount(DecimalInput, { - propsData: { modelValue: 1 }, + propsData: { modelValue: 1.2 }, }); - // Assert value is 1.0 - expect(wrapper.find('input').element.value).to.equal('1.0'); -}); - -test('up arrow should increment value by step', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { step: 0.2 }, - }); - - // Press up arrow - await wrapper.trigger('keydown', { key: 'ArrowUp' }); - - // Assert value is 0.2 and input event was emitted - expect(wrapper.find('input').element.value).to.equal('0.2'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0.2]]); -}); - -test('down arrow should increment value by step', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { step: 0.2 }, - }); - - // Press down arrow - await wrapper.trigger('keydown', { key: 'ArrowDown' }); - - // Assert value is -0.2 and input event was emitted - expect(wrapper.find('input').element.value).to.equal('-0.2'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[-0.2]]); + // Assert value is 1.2 + expect(wrapper.find('input').element.value).to.equal('1.2'); }); test('should fire input event when value changes', async () => { // Initialize component const wrapper = mount(DecimalInput); - // Set value to 1 - wrapper.find('input').element.value = '1.0'; - await wrapper.find('input').trigger('input'); + // Set value to 1.2 + await wrapper.find('input').setValue('1.2') // Assert input event was emitted - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[1.0]]); -}); - -test('should accept numerical values', async () => { - // Initialize component - const wrapper = mount(DecimalInput); - - // 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()['update:modelValue']).to.deep.equal([[1.0]]); + expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[1.2]]); }); -test('should accept decimal values', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { modelValue: 1 }, - }); - - // Try to set value to 1.5 - wrapper.find('input').element.value = '1.5'; - await wrapper.find('input').trigger('input'); - - // Assert value was accepted and input event was emitted - expect(wrapper.find('input').element.value).to.equal('1.5'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[1.5]]); -}); - -test('should not accept non numerical values', async () => { +test('should format value according to padding and digits props', async () => { // Initialize component const wrapper = mount(DecimalInput, { - propsData: { modelValue: 1 }, + propsData: { padding: 2, digits: 3 }, }); - // 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.0'); - expect(wrapper.emitted()['update:modelValue']).to.equal(undefined); + // Assert value is correctly formatted + expect(wrapper.find('input').element.value).to.equal('00.000'); }); test('should format input value on blur', async () => { // Initialize component const wrapper = mount(DecimalInput, { - propsData: { modelValue: 1, padding: 3, digits: 2 }, - }); - - // 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()['update:modelValue']).to.equal(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('001.00'); - expect(wrapper.emitted()['update:modelValue']).to.equal(undefined); -}); - -test('should allow input to be empty until blur', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { modelValue: 5 }, + propsData: { modelValue: 1, padding: 2, digits: 2 }, }); - // Set value to '' - wrapper.find('input').element.value = ''; - await wrapper.find('input').trigger('input'); + // Set value to '1.2' + await wrapper.find('input').setValue('1.2'); - // Assert value is '' and input event was emitted with default value - expect(wrapper.find('input').element.value).to.equal(''); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0.0]]); + // Assert value was not updated + expect(wrapper.find('input').element.value).to.equal('1.2'); // 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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0.0]]); -}); - -test('should allow input to be "-" until blur', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { modelValue: 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()['update:modelValue']).to.deep.equal([[0.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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0.0]]); -}); - -test('should allow input to be "." until blur', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { modelValue: 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()['update:modelValue']).to.deep.equal([[0.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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0.0]]); -}); - -test('default value should be the minimum if 0.0 is not valid', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { modelValue: 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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[2.0]]); -}); - -test('should not allow input to be below the minimum', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { min: 10, modelValue: 20 }, - }); - - // Try to set value to 9, which is below the minimum - wrapper.find('input').element.value = '9.0'; - await wrapper.find('input').trigger('input'); - - // Assert value is 10 and input event was emitted - expect(wrapper.find('input').element.value).to.equal('10.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[10.0]]); - - // 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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[10.0]]); -}); - -test('should not allow input to be above the maximum', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { max: 10 }, - }); - - // Try to set value to 11, which is above the maximum - wrapper.find('input').element.value = '11.0'; - await wrapper.find('input').trigger('input'); - - // Assert value is 10 and input event was emitted - expect(wrapper.find('input').element.value).to.equal('10.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[10.0]]); - - // 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.0'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[10.0]]); -}); - -test('should format value according to padding and digits props', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { padding: 2, digits: 3 }, - }); - - // Assert value is correctly formatted - expect(wrapper.find('input').element.value).to.equal('00.000'); -}); - -test('should emit keydown event if arrow-keys is false', async () => { - // Initialize component - const wrapper = mount(DecimalInput, { - propsData: { arrowKeys: false }, - }); - - // Try to increment value - await wrapper.trigger('keydown', { key: 'ArrowUp' }); - - // Assert keydown event emitted - expect(wrapper.emitted().keydown.length).to.equal(1); + // Assert value was formatted + expect(wrapper.find('input').element.value).to.equal('01.20'); }); diff --git a/tests/unit/components/IntegerInput.spec.js b/tests/unit/components/IntegerInput.spec.js @@ -2,50 +2,14 @@ import { test, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import IntegerInput from '@/components/IntegerInput.vue'; -test('value should be 0 by default', () => { - // Initialize component - const wrapper = mount(IntegerInput); - - // Assert value is 0 - expect(wrapper.find('input').element.value).to.equal('0'); -}); - -test('should read value prop', () => { +test('should be initialized to modelValue', () => { // Initialize component const wrapper = mount(IntegerInput, { - propsData: { modelValue: 1 }, + propsData: { modelValue: 123 }, }); // Assert value is 1 - expect(wrapper.find('input').element.value).to.equal('1'); -}); - -test('up arrow should increment value by step', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { step: 2 }, - }); - - // 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('2'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[2]]); -}); - -test('down arrow should increment value by step', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { step: 2 }, - }); - - // Press down arrow - await wrapper.trigger('keydown', { key: 'ArrowDown' }); - - // Assert value is -1 and input event was emitted - expect(wrapper.find('input').element.value).to.equal('-2'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[-2]]); + expect(wrapper.find('input').element.value).to.equal('123'); }); test('should fire input event when value changes', async () => { @@ -53,201 +17,37 @@ test('should fire input event when value changes', async () => { const wrapper = mount(IntegerInput); // Set value to 1 - wrapper.find('input').element.value = '1'; - await wrapper.find('input').trigger('input'); + await wrapper.find('input').setValue('1'); // Assert input event was emitted expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[1]]); }); -test('should accept numerical values', async () => { - // Initialize component - const wrapper = mount(IntegerInput); - - // 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()['update:modelValue']).to.deep.equal([[1]]); -}); - -test('should not accept decimal values', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { modelValue: 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()['update:modelValue']).to.equal(undefined); -}); - -test('should not accept non numerical values', async () => { +test('should format value according to padding prop', async () => { // Initialize component const wrapper = mount(IntegerInput, { - propsData: { modelValue: 1 }, + propsData: { padding: 2 }, }); - // 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()['update:modelValue']).to.equal(undefined); + // Assert value is correctly formatted + expect(wrapper.find('input').element.value).to.equal('00'); }); test('should format input value on blur', async () => { // Initialize component const wrapper = mount(IntegerInput, { - propsData: { modelValue: 1, padding: 3 }, - }); - - // 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()['update:modelValue']).to.equal(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('001'); - expect(wrapper.emitted()['update:modelValue']).to.equal(undefined); -}); - -test('should allow input to be empty until blur', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { modelValue: 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()['update:modelValue']).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()['update:modelValue']).to.deep.equal([[0]]); -}); - -test('should allow input to be "-" until blur', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { modelValue: 5 }, + propsData: { modelValue: 1, padding: 2 }, }); - // Set value to '-' - wrapper.find('input').element.value = '-'; - await wrapper.find('input').trigger('input'); + // Set value to '2' + await wrapper.find('input').setValue('2'); - // Assert value is '-' and input event was emitted with default value - expect(wrapper.find('input').element.value).to.equal('-'); - expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[0]]); + // Assert value was not updated + expect(wrapper.find('input').element.value).to.equal('2'); // 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()['update:modelValue']).to.deep.equal([[0]]); -}); - -test('default value should be the minimum if 0 is not valid', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { modelValue: 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()['update:modelValue']).to.deep.equal([[2]]); -}); - -test('should not allow input to be below the minimum', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { min: 10, modelValue: 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()['update:modelValue']).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()['update:modelValue']).to.deep.equal([[10]]); -}); - -test('should not allow input to be above the maximum', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - 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()['update:modelValue']).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()['update:modelValue']).to.deep.equal([[10]]); -}); - -test('should format value according to padding prop', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { padding: 2 }, - }); - - // Assert value is correctly formatted - expect(wrapper.find('input').element.value).to.equal('00'); -}); - -test('should emit keydown event if arrow-keys is false', async () => { - // Initialize component - const wrapper = mount(IntegerInput, { - propsData: { arrowKeys: false }, - }); - - // Try to increment value - await wrapper.trigger('keydown', { key: 'ArrowUp' }); - - // Assert keydown event emitted - expect(wrapper.emitted().keydown.length).to.equal(1); + // Assert value was formatted + expect(wrapper.find('input').element.value).to.equal('02'); });