test: Tests logic refactor, typehinting improvements
This commit is contained in:
parent
9a344bf8b5
commit
320df96b96
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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<string, string[]>
|
||||
@ -49,7 +48,7 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
public proxy: Record<string, unknown> = {}
|
||||
|
||||
private registry: PathRegistry<FormularioField> = new PathRegistry()
|
||||
private registry: PathRegistry<FormularioFieldInterface> = 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<ViolationsRecord> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
10
src/types.ts
Normal file
10
src/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Violation } from '@/validation/validator'
|
||||
|
||||
export interface FormularioFieldInterface {
|
||||
hasModel: boolean;
|
||||
model: unknown;
|
||||
proxy: unknown;
|
||||
setErrors(errors: string[]): void;
|
||||
runValidation(): Promise<Violation[]>;
|
||||
resetValidation(): void;
|
||||
}
|
@ -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<string, unknown>)
|
||||
}
|
||||
return []
|
||||
}
|
@ -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'
|
||||
|
@ -19,139 +19,89 @@ Vue.use(Formulario, {
|
||||
})
|
||||
|
||||
describe('FormularioField', () => {
|
||||
it('Allows custom field-rule level validation strings', async () => {
|
||||
const wrapper = mount(FormularioField, {
|
||||
const createWrapper = (props = {}) => mount(FormularioField, {
|
||||
propsData: {
|
||||
name: 'test',
|
||||
value: 'other value',
|
||||
name: 'field',
|
||||
value: 'initial',
|
||||
validation: 'required|in:abcdef',
|
||||
validationMessages: { in: 'the value was different than expected' },
|
||||
validationBehavior: 'live',
|
||||
...props,
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="violation in props.context.violations">{{ violation.message }}</span></div>`
|
||||
},
|
||||
})
|
||||
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: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
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: `<div>
|
||||
default: `
|
||||
<div>
|
||||
<input type="text" v-model="props.context.model">
|
||||
<span v-for="error in props.context.violations">{{ error.message }}</span>
|
||||
</div>`
|
||||
}
|
||||
<span v-for="(violation, index) in props.context.violations" :key="index" data-violation>
|
||||
{{ violation.message }}
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
|
||||
wrapper.find('input[type="text"]').element['value'] = 'Test'
|
||||
wrapper.find('input[type="text"]').trigger('change')
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('input[type="text"]').element['value']).toBe('Test')
|
||||
expect(wrapper.find('span').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.` },
|
||||
test('allows custom field-rule level validation strings', async () => {
|
||||
const wrapper = createWrapper({
|
||||
validationBehavior: 'live',
|
||||
value: 'other value'
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').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: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
|
||||
expect(wrapper.find('[data-violation]').text()).toBe(
|
||||
'the value was different than expected'
|
||||
)
|
||||
})
|
||||
|
||||
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' },
|
||||
test.each([
|
||||
['demand'],
|
||||
['submit'],
|
||||
])('no validation when validationBehavior is not "live"', async validationBehavior => {
|
||||
const wrapper = createWrapper({ validationBehavior })
|
||||
|
||||
await flushPromises()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
test('allows custom validation rule message', async () => {
|
||||
const wrapper = createWrapper({
|
||||
value: 'other value',
|
||||
validationMessages: { in: ({ value }) => `the string "${value}" is not correct` },
|
||||
validationBehavior: 'live',
|
||||
value: 'bar'
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
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' },
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-violation]').text()).toBe(
|
||||
'the string "other value" is not correct'
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').text()).toBe('failed the foobar check')
|
||||
})
|
||||
|
||||
it('uses global custom validation rules', async () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-violation]').text()).toBe('failed the custom rule check')
|
||||
})
|
||||
|
||||
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: `
|
||||
<FormularioField
|
||||
name="test"
|
||||
:validation="validation"
|
||||
validation-behavior="live"
|
||||
v-slot="{ context }"
|
||||
>
|
||||
<div v-for="(_, index) in context.violations" :key="index" data-violation />
|
||||
</FormularioField>
|
||||
`
|
||||
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: `
|
||||
<FormularioField
|
||||
name="test"
|
||||
value="123"
|
||||
validation="^required|in:xyz|min:10,length"
|
||||
validation-behavior="live"
|
||||
v-slot="{ context }"
|
||||
>
|
||||
<div v-for="(_, index) in context.violations" :key="index" data-violation />
|
||||
</FormularioField>
|
||||
`
|
||||
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: `
|
||||
<FormularioField
|
||||
v-slot="{ context }"
|
||||
name="testinput"
|
||||
name="field"
|
||||
validation="required"
|
||||
validation-behavior="submit"
|
||||
>
|
||||
<span v-for="error in context.violations">{{ error.message }}</span>
|
||||
<span v-for="(violation, index) in context.violations" :key="index" data-violation>
|
||||
{{ violation.message }}
|
||||
</span>
|
||||
</FormularioField>
|
||||
`
|
||||
}
|
||||
@ -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: `
|
||||
<FormularioForm v-model="values">
|
||||
<FormularioField v-slot="{ context }" :model-get-converter="onGet" name="test" >
|
||||
<span>{{ context.model }}</span>
|
||||
<FormularioForm v-model="state">
|
||||
<FormularioField
|
||||
v-slot="{ context }"
|
||||
:model-get-converter="onGet"
|
||||
name="date"
|
||||
>
|
||||
<span data-output>{{ context.model }}</span>
|
||||
</FormularioField>
|
||||
</FormularioForm>
|
||||
`,
|
||||
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: `
|
||||
<FormularioForm v-model="values">
|
||||
<FormularioForm v-model="state">
|
||||
<FormularioField
|
||||
v-slot="{ context }"
|
||||
:model-get-converter="onGet"
|
||||
@ -331,15 +275,20 @@ describe('FormularioField', () => {
|
||||
|
||||
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') }],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user