diff --git a/build/rollup.config.js b/build/rollup.config.js index 5feea19..dd7e451 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -21,7 +21,7 @@ export default { }], external: ['nanoid/non-secure', 'vue', 'vue-property-decorator'], plugins: [ - typescript({ check: false, sourceMap: false }), + typescript({ sourceMap: false }), vue({ css: true, compileTemplate: true }), alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }), commonjs(), diff --git a/build/rollup.iife.config.js b/build/rollup.iife.config.js index dc3517a..44fb05d 100644 --- a/build/rollup.iife.config.js +++ b/build/rollup.iife.config.js @@ -27,7 +27,7 @@ export default { browser: true, preferBuiltins: false, }), - typescript({ check: false, sourceMap: false }), + typescript({ sourceMap: false }), vue({ css: true, compileTemplate: true }), alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }), commonjs(), diff --git a/src/FormularioField.vue b/src/FormularioField.vue index b628571..4cff96b 100644 --- a/src/FormularioField.vue +++ b/src/FormularioField.vue @@ -13,7 +13,7 @@ import { Prop, Watch, } from 'vue-property-decorator' -import { arrayify, has, shallowEquals, snakeToCamel } from './utils' +import { has, shallowEquals, snakeToCamel } from './utils' import { processConstraints, validate, diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index b076d62..b38746d 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -24,8 +24,7 @@ import { import PathRegistry from '@/PathRegistry' -import FormularioField from '@/FormularioField.vue' - +import { FormularioFieldInterface } from '@/types' import { Violation } from '@/validation/validator' type ErrorsRecord = Record @@ -49,7 +48,7 @@ export default class FormularioForm extends Vue { public proxy: Record = {} - private registry: PathRegistry = new PathRegistry() + private registry: PathRegistry = new PathRegistry() // Local error messages are temporal, they wiped each resetValidation call private localFieldsErrors: ErrorsRecord = {} @@ -80,7 +79,7 @@ export default class FormularioForm extends Vue { } @Provide('__FormularioForm_register') - private register (path: string, field: FormularioField): void { + private register (path: string, field: FormularioFieldInterface): void { this.registry.add(path, field) const value = getNested(this.modelCopy, path) @@ -142,7 +141,7 @@ export default class FormularioForm extends Vue { public runValidation (): Promise { const violations: ViolationsRecord = {} - const runs = this.registry.map((field: FormularioField, path: string) => { + const runs = this.registry.map((field: FormularioFieldInterface, path: string) => { return field.runValidation().then(v => { violations[path] = v }) }) @@ -163,7 +162,7 @@ export default class FormularioForm extends Vue { public resetValidation (): void { this.localFieldsErrors = {} this.localFormErrors = [] - this.registry.forEach((field: FormularioField) => { + this.registry.forEach((field: FormularioFieldInterface) => { field.resetValidation() }) } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9cad847 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,10 @@ +import { Violation } from '@/validation/validator' + +export interface FormularioFieldInterface { + hasModel: boolean; + model: unknown; + proxy: unknown; + setErrors(errors: string[]): void; + runValidation(): Promise; + resetValidation(): void; +} diff --git a/src/utils/arrayify.ts b/src/utils/arrayify.ts deleted file mode 100644 index f659c16..0000000 --- a/src/utils/arrayify.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Converts to array. - * If given parameter is not string, object ot array, result will be an empty array. - * @param {*} item - */ -export default function arrayify (item: unknown): unknown[] { - if (!item) { - return [] - } - if (typeof item === 'string') { - return [item] - } - if (Array.isArray(item)) { - return item - } - if (typeof item === 'object') { - return Object.values(item as Record) - } - return [] -} diff --git a/src/utils/index.ts b/src/utils/index.ts index a5582e3..0c48322 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,3 @@ -export { default as arrayify } from './arrayify' export { default as clone } from './clone' export { default as has } from './has' export { isScalar } from './types' diff --git a/test/unit/FormularioField.test.js b/test/unit/FormularioField.test.js index f49f4b8..af77671 100644 --- a/test/unit/FormularioField.test.js +++ b/test/unit/FormularioField.test.js @@ -19,139 +19,89 @@ Vue.use(Formulario, { }) describe('FormularioField', () => { - it('Allows custom field-rule level validation strings', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - value: 'other value', - validation: 'required|in:abcdef', - validationMessages: { in: 'the value was different than expected' }, - validationBehavior: 'live', - }, - scopedSlots: { - default: `
{{ violation.message }}
` - }, - }) - await flushPromises() - expect(wrapper.find('span').text()).toBe('the value was different than expected') - }) - - it('No validation on created when validationBehavior is not live', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - validation: 'required|in:abcdef', - validationMessages: {in: 'the value was different than expected'}, - value: 'other value' - }, - scopedSlots: { - default: `
{{ error.message }}
` - } - }) - await flushPromises() - expect(wrapper.find('span').exists()).toBe(false) - }) - - it('No validation on value change when validationBehavior is "submit"', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - validation: 'required|in:abcdef', - validationMessages: {in: 'the value was different than expected'}, - validationBehavior: 'submit', - value: 'Initial' - }, - scopedSlots: { - default: `
+ const createWrapper = (props = {}) => mount(FormularioField, { + propsData: { + name: 'field', + value: 'initial', + validation: 'required|in:abcdef', + validationMessages: { in: 'the value was different than expected' }, + ...props, + }, + scopedSlots: { + default: ` +
- {{ error.message }} -
` - } + + {{ violation.message }} + +
+ `, + }, + }) + + test('allows custom field-rule level validation strings', async () => { + const wrapper = createWrapper({ + validationBehavior: 'live', }) await flushPromises() - expect(wrapper.find('span').exists()).toBe(false) + expect(wrapper.find('[data-violation]').text()).toBe( + 'the value was different than expected' + ) + }) - wrapper.find('input[type="text"]').element['value'] = 'Test' - wrapper.find('input[type="text"]').trigger('change') + test.each([ + ['demand'], + ['submit'], + ])('no validation when validationBehavior is not "live"', async validationBehavior => { + const wrapper = createWrapper({ validationBehavior }) await flushPromises() - expect(wrapper.find('input[type="text"]').element['value']).toBe('Test') - expect(wrapper.find('span').exists()).toBe(false) + expect(wrapper.find('[data-violation]').exists()).toBe(false) + + wrapper.find('input').element['value'] = 'updated' + wrapper.find('input').trigger('change') + + await flushPromises() + + expect(wrapper.find('input').element['value']).toBe('updated') + expect(wrapper.find('[data-violation]').exists()).toBe(false) }) - it('Allows custom field-rule level validation functions', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - validation: 'required|in:abcdef', - validationMessages: { in: ({ value }) => `The string ${value} is not correct.` }, - validationBehavior: 'live', - value: 'other value' - }, - scopedSlots: { - default: `
{{ error.message }}
` - } + test('allows custom validation rule message', async () => { + const wrapper = createWrapper({ + value: 'other value', + validationMessages: { in: ({ value }) => `the string "${value}" is not correct` }, + validationBehavior: 'live', }) + await flushPromises() - expect(wrapper.find('span').text()).toBe('The string other value is not correct.') + + expect(wrapper.find('[data-violation]').text()).toBe( + 'the string "other value" is not correct' + ) }) - it('No validation on created when validationBehavior is default', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - validation: 'required|in:abcdef', - validationMessages: { in: 'the value was different than expected' }, - value: 'other value' - }, - scopedSlots: { - default: `
{{ error.message }}
` - } + test.each([ + ['bar', ({ value }) => value === 'foo'], + ['bar', ({ value }) => Promise.resolve(value === 'foo')], + ])('uses local custom validation rules', async (value, rule) => { + const wrapper = createWrapper({ + value, + validation: 'required|custom', + validationRules: { custom: rule }, + validationMessages: { custom: 'failed the custom rule check' }, + validationBehavior: 'live', }) + await flushPromises() - expect(wrapper.find('span').exists()).toBe(false) + + expect(wrapper.find('[data-violation]').text()).toBe('failed the custom rule check') }) - it('Uses custom async validation rules on defined on the field', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - validation: 'required|foobar', - validationRules: { foobar: async ({ value }) => value === 'foo' }, - validationMessages: { foobar: 'failed the foobar check' }, - validationBehavior: 'live', - value: 'bar' - }, - scopedSlots: { - default: `
{{ error.message }}
` - } - }) - await flushPromises() - expect(wrapper.find('span').text()).toBe('failed the foobar check') - }) - - it('Uses custom sync validation rules on defined on the field', async () => { - const wrapper = mount(FormularioField, { - propsData: { - name: 'test', - value: 'bar', - validation: 'required|foobar', - validationRules: { foobar: ({ value }) => value === 'foo' }, - validationMessages: { foobar: 'failed the foobar check' }, - validationBehavior: 'live', - }, - scopedSlots: { - default: `
{{ error.message }}
` - } - }) - await flushPromises() - expect(wrapper.find('span').text()).toBe('failed the foobar check') - }) - - it('uses global custom validation rules', async () => { + test('uses global custom validation rules', async () => { mount(FormularioField, { propsData: { name: 'test', @@ -160,14 +110,16 @@ describe('FormularioField', () => { validationBehavior: 'live', }, }) + await flushPromises() + expect(globalRule.mock.calls.length).toBe(1) }) - it('emits correct validation event', async () => { + test('emits correct validation event', async () => { const wrapper = mount(FormularioField, { propsData: { - name: 'fieldName', + name: 'field', value: '', validation: 'required', validationBehavior: 'live', @@ -177,11 +129,15 @@ describe('FormularioField', () => { await flushPromises() expect(wrapper.emitted('validation')).toEqual([[{ - name: 'fieldName', + name: 'field', violations: [{ - rule: expect.stringContaining('required'), + rule: 'required', args: expect.any(Array), - context: expect.any(Object), + context: { + name: 'field', + value: '', + formValues: {}, + }, message: expect.any(String), }], }]]) @@ -196,18 +152,10 @@ describe('FormularioField', () => { validation, expectedViolationsCount ) => { - const wrapper = mount({ - data: () => ({ validation }), - template: ` - -
- - ` + const wrapper = createWrapper({ + value: '', + validation, + validationBehavior: 'live', }) await flushPromises() @@ -215,19 +163,11 @@ describe('FormularioField', () => { expect(wrapper.findAll('[data-violation]').length).toBe(expectedViolationsCount) }) - it('proceeds validation if passed a rule with bail modifier', async () => { - const wrapper = mount({ - template: ` - -
- - ` + test('proceeds validation if passed a rule with bail modifier', async () => { + const wrapper = createWrapper({ + value: '123', + validation: '^required|in:xyz|min:10,length', + validationBehavior: 'live', }) await flushPromises() @@ -235,18 +175,19 @@ describe('FormularioField', () => { expect(wrapper.findAll('[data-violation]').length).toBe(2) }) - it('Displays errors when validation-behavior is submit and form is submitted', async () => { + test('displays errors when validation-behavior is submit and form is submitted', async () => { const wrapper = mount(FormularioForm, { - propsData: { name: 'test' }, slots: { default: ` - {{ error.message }} + + {{ violation.message }} + ` } @@ -254,49 +195,52 @@ describe('FormularioField', () => { await flushPromises() - expect(wrapper.find('span').exists()).toBe(false) + expect(wrapper.find('[data-violation]').exists()).toBe(false) wrapper.trigger('submit') await flushPromises() - expect(wrapper.find('span').exists()).toBe(true) + expect(wrapper.find('[data-violation]').exists()).toBe(true) }) - it('Model getter for input', async () => { + test('model getter for input', async () => { const wrapper = mount({ - data: () => ({ values: { test: 'abcd' } }), + data: () => ({ state: { date: 'not a date' } }), template: ` - - - {{ context.model }} + + + {{ context.model }} `, methods: { - onGet(source) { - if (!(source instanceof Date)) { - return 'invalid date' - } - - return source.getDate() - } + onGet (source) { + return source instanceof Date ? source.getDate() : 'invalid date' + }, } }) await flushPromises() - expect(wrapper.find('span').text()).toBe('invalid date') - wrapper.vm.values = { test: new Date('1995-12-17') } + expect(wrapper.find('[data-output]').text()).toBe('invalid date') + + wrapper.vm.state = { date: new Date('1995-12-17') } + await flushPromises() - expect(wrapper.find('span').text()).toBe('17') + + expect(wrapper.find('[data-output]').text()).toBe('17') }) - it('Model setter for input', async () => { + test('model setter for input', async () => { const wrapper = mount({ - data: () => ({ values: { date: 'not a date' } }), + data: () => ({ state: { date: 'not a date' } }), template: ` - + { await flushPromises() - expect(wrapper.vm.values.date).toBe(undefined) - - wrapper.find('input[type="text"]').element['value'] = '12' - wrapper.find('input[type="text"]').trigger('input') + wrapper.find('input').setValue('12') + wrapper.find('input').trigger('input') await flushPromises() - expect(wrapper.vm.values.date.toISOString()).toBe( - (new Date('2001-05-12')).toISOString() - ) + const form = wrapper.findComponent(FormularioForm) + + // @TODO: investigate where redundant events come from + expect(form.emitted('input')).toEqual([ + [{}], + [{}], + [{ date: new Date('2001-05-12') }], + [{ date: new Date('2001-05-12') }], + [{ date: new Date('2001-05-12') }], + ]) }) }) diff --git a/test/unit/PathRegistry.test.js b/test/unit/PathRegistry.test.js index b802746..5bce004 100644 --- a/test/unit/PathRegistry.test.js +++ b/test/unit/PathRegistry.test.js @@ -1,7 +1,7 @@ import PathRegistry from '@/PathRegistry' describe('PathRegistry', () => { - test ('subset structure', () => { + test('subset structure', () => { const registry = new PathRegistry() const paths = path => Array.from(registry.getSubset(path).paths())