1
0
mirror of synced 2024-11-22 05:16:05 +03:00

test: Tests logic refactor, typehinting improvements

This commit is contained in:
Zaytsev Kirill 2021-05-25 21:37:42 +03:00
parent 9a344bf8b5
commit 320df96b96
9 changed files with 143 additions and 206 deletions

View File

@ -21,7 +21,7 @@ export default {
}], }],
external: ['nanoid/non-secure', 'vue', 'vue-property-decorator'], external: ['nanoid/non-secure', 'vue', 'vue-property-decorator'],
plugins: [ plugins: [
typescript({ check: false, sourceMap: false }), typescript({ sourceMap: false }),
vue({ css: true, compileTemplate: true }), vue({ css: true, compileTemplate: true }),
alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }), alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }),
commonjs(), commonjs(),

View File

@ -27,7 +27,7 @@ export default {
browser: true, browser: true,
preferBuiltins: false, preferBuiltins: false,
}), }),
typescript({ check: false, sourceMap: false }), typescript({ sourceMap: false }),
vue({ css: true, compileTemplate: true }), vue({ css: true, compileTemplate: true }),
alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }), alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }),
commonjs(), commonjs(),

View File

@ -13,7 +13,7 @@ import {
Prop, Prop,
Watch, Watch,
} from 'vue-property-decorator' } from 'vue-property-decorator'
import { arrayify, has, shallowEquals, snakeToCamel } from './utils' import { has, shallowEquals, snakeToCamel } from './utils'
import { import {
processConstraints, processConstraints,
validate, validate,

View File

@ -24,8 +24,7 @@ import {
import PathRegistry from '@/PathRegistry' import PathRegistry from '@/PathRegistry'
import FormularioField from '@/FormularioField.vue' import { FormularioFieldInterface } from '@/types'
import { Violation } from '@/validation/validator' import { Violation } from '@/validation/validator'
type ErrorsRecord = Record<string, string[]> type ErrorsRecord = Record<string, string[]>
@ -49,7 +48,7 @@ export default class FormularioForm extends Vue {
public proxy: Record<string, unknown> = {} 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 // Local error messages are temporal, they wiped each resetValidation call
private localFieldsErrors: ErrorsRecord = {} private localFieldsErrors: ErrorsRecord = {}
@ -80,7 +79,7 @@ export default class FormularioForm extends Vue {
} }
@Provide('__FormularioForm_register') @Provide('__FormularioForm_register')
private register (path: string, field: FormularioField): void { private register (path: string, field: FormularioFieldInterface): void {
this.registry.add(path, field) this.registry.add(path, field)
const value = getNested(this.modelCopy, path) const value = getNested(this.modelCopy, path)
@ -142,7 +141,7 @@ export default class FormularioForm extends Vue {
public runValidation (): Promise<ViolationsRecord> { public runValidation (): Promise<ViolationsRecord> {
const violations: 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 }) return field.runValidation().then(v => { violations[path] = v })
}) })
@ -163,7 +162,7 @@ export default class FormularioForm extends Vue {
public resetValidation (): void { public resetValidation (): void {
this.localFieldsErrors = {} this.localFieldsErrors = {}
this.localFormErrors = [] this.localFormErrors = []
this.registry.forEach((field: FormularioField) => { this.registry.forEach((field: FormularioFieldInterface) => {
field.resetValidation() field.resetValidation()
}) })
} }

10
src/types.ts Normal file
View 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;
}

View File

@ -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 []
}

View File

@ -1,4 +1,3 @@
export { default as arrayify } from './arrayify'
export { default as clone } from './clone' export { default as clone } from './clone'
export { default as has } from './has' export { default as has } from './has'
export { isScalar } from './types' export { isScalar } from './types'

View File

@ -19,139 +19,89 @@ Vue.use(Formulario, {
}) })
describe('FormularioField', () => { describe('FormularioField', () => {
it('Allows custom field-rule level validation strings', async () => { const createWrapper = (props = {}) => mount(FormularioField, {
const wrapper = mount(FormularioField, { propsData: {
propsData: { name: 'field',
name: 'test', value: 'initial',
value: 'other value', validation: 'required|in:abcdef',
validation: 'required|in:abcdef', validationMessages: { in: 'the value was different than expected' },
validationMessages: { in: 'the value was different than expected' }, ...props,
validationBehavior: 'live', },
}, scopedSlots: {
scopedSlots: { default: `
default: `<div><span v-for="violation in props.context.violations">{{ violation.message }}</span></div>` <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>
<input type="text" v-model="props.context.model"> <input type="text" v-model="props.context.model">
<span v-for="error in props.context.violations">{{ error.message }}</span> <span v-for="(violation, index) in props.context.violations" :key="index" data-violation>
</div>` {{ violation.message }}
} </span>
</div>
`,
},
})
test('allows custom field-rule level validation strings', async () => {
const wrapper = createWrapper({
validationBehavior: 'live',
}) })
await flushPromises() 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' test.each([
wrapper.find('input[type="text"]').trigger('change') ['demand'],
['submit'],
])('no validation when validationBehavior is not "live"', async validationBehavior => {
const wrapper = createWrapper({ validationBehavior })
await flushPromises() await flushPromises()
expect(wrapper.find('input[type="text"]').element['value']).toBe('Test') expect(wrapper.find('[data-violation]').exists()).toBe(false)
expect(wrapper.find('span').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 () => { test('allows custom validation rule message', async () => {
const wrapper = mount(FormularioField, { const wrapper = createWrapper({
propsData: { value: 'other value',
name: 'test', validationMessages: { in: ({ value }) => `the string "${value}" is not correct` },
validation: 'required|in:abcdef', validationBehavior: 'live',
validationMessages: { in: ({ value }) => `The string ${value} is not correct.` },
validationBehavior: 'live',
value: 'other value'
},
scopedSlots: {
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>`
}
}) })
await flushPromises() 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 () => { test.each([
const wrapper = mount(FormularioField, { ['bar', ({ value }) => value === 'foo'],
propsData: { ['bar', ({ value }) => Promise.resolve(value === 'foo')],
name: 'test', ])('uses local custom validation rules', async (value, rule) => {
validation: 'required|in:abcdef', const wrapper = createWrapper({
validationMessages: { in: 'the value was different than expected' }, value,
value: 'other value' validation: 'required|custom',
}, validationRules: { custom: rule },
scopedSlots: { validationMessages: { custom: 'failed the custom rule check' },
default: `<div><span v-for="error in props.context.violations">{{ error.message }}</span></div>` validationBehavior: 'live',
}
}) })
await flushPromises() 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 () => { test('uses global custom validation rules', 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: `<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' },
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 () => {
mount(FormularioField, { mount(FormularioField, {
propsData: { propsData: {
name: 'test', name: 'test',
@ -160,14 +110,16 @@ describe('FormularioField', () => {
validationBehavior: 'live', validationBehavior: 'live',
}, },
}) })
await flushPromises() await flushPromises()
expect(globalRule.mock.calls.length).toBe(1) expect(globalRule.mock.calls.length).toBe(1)
}) })
it('emits correct validation event', async () => { test('emits correct validation event', async () => {
const wrapper = mount(FormularioField, { const wrapper = mount(FormularioField, {
propsData: { propsData: {
name: 'fieldName', name: 'field',
value: '', value: '',
validation: 'required', validation: 'required',
validationBehavior: 'live', validationBehavior: 'live',
@ -177,11 +129,15 @@ describe('FormularioField', () => {
await flushPromises() await flushPromises()
expect(wrapper.emitted('validation')).toEqual([[{ expect(wrapper.emitted('validation')).toEqual([[{
name: 'fieldName', name: 'field',
violations: [{ violations: [{
rule: expect.stringContaining('required'), rule: 'required',
args: expect.any(Array), args: expect.any(Array),
context: expect.any(Object), context: {
name: 'field',
value: '',
formValues: {},
},
message: expect.any(String), message: expect.any(String),
}], }],
}]]) }]])
@ -196,18 +152,10 @@ describe('FormularioField', () => {
validation, validation,
expectedViolationsCount expectedViolationsCount
) => { ) => {
const wrapper = mount({ const wrapper = createWrapper({
data: () => ({ validation }), value: '',
template: ` validation,
<FormularioField validationBehavior: 'live',
name="test"
:validation="validation"
validation-behavior="live"
v-slot="{ context }"
>
<div v-for="(_, index) in context.violations" :key="index" data-violation />
</FormularioField>
`
}) })
await flushPromises() await flushPromises()
@ -215,19 +163,11 @@ describe('FormularioField', () => {
expect(wrapper.findAll('[data-violation]').length).toBe(expectedViolationsCount) expect(wrapper.findAll('[data-violation]').length).toBe(expectedViolationsCount)
}) })
it('proceeds validation if passed a rule with bail modifier', async () => { test('proceeds validation if passed a rule with bail modifier', async () => {
const wrapper = mount({ const wrapper = createWrapper({
template: ` value: '123',
<FormularioField validation: '^required|in:xyz|min:10,length',
name="test" validationBehavior: 'live',
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>
`
}) })
await flushPromises() await flushPromises()
@ -235,18 +175,19 @@ describe('FormularioField', () => {
expect(wrapper.findAll('[data-violation]').length).toBe(2) 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, { const wrapper = mount(FormularioForm, {
propsData: { name: 'test' },
slots: { slots: {
default: ` default: `
<FormularioField <FormularioField
v-slot="{ context }" v-slot="{ context }"
name="testinput" name="field"
validation="required" validation="required"
validation-behavior="submit" 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> </FormularioField>
` `
} }
@ -254,49 +195,52 @@ describe('FormularioField', () => {
await flushPromises() await flushPromises()
expect(wrapper.find('span').exists()).toBe(false) expect(wrapper.find('[data-violation]').exists()).toBe(false)
wrapper.trigger('submit') wrapper.trigger('submit')
await flushPromises() 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({ const wrapper = mount({
data: () => ({ values: { test: 'abcd' } }), data: () => ({ state: { date: 'not a date' } }),
template: ` template: `
<FormularioForm v-model="values"> <FormularioForm v-model="state">
<FormularioField v-slot="{ context }" :model-get-converter="onGet" name="test" > <FormularioField
<span>{{ context.model }}</span> v-slot="{ context }"
:model-get-converter="onGet"
name="date"
>
<span data-output>{{ context.model }}</span>
</FormularioField> </FormularioField>
</FormularioForm> </FormularioForm>
`, `,
methods: { methods: {
onGet(source) { onGet (source) {
if (!(source instanceof Date)) { return source instanceof Date ? source.getDate() : 'invalid date'
return 'invalid date' },
}
return source.getDate()
}
} }
}) })
await flushPromises() 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() 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({ const wrapper = mount({
data: () => ({ values: { date: 'not a date' } }), data: () => ({ state: { date: 'not a date' } }),
template: ` template: `
<FormularioForm v-model="values"> <FormularioForm v-model="state">
<FormularioField <FormularioField
v-slot="{ context }" v-slot="{ context }"
:model-get-converter="onGet" :model-get-converter="onGet"
@ -331,15 +275,20 @@ describe('FormularioField', () => {
await flushPromises() await flushPromises()
expect(wrapper.vm.values.date).toBe(undefined) wrapper.find('input').setValue('12')
wrapper.find('input').trigger('input')
wrapper.find('input[type="text"]').element['value'] = '12'
wrapper.find('input[type="text"]').trigger('input')
await flushPromises() await flushPromises()
expect(wrapper.vm.values.date.toISOString()).toBe( const form = wrapper.findComponent(FormularioForm)
(new Date('2001-05-12')).toISOString()
) // @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') }],
])
}) })
}) })

View File

@ -1,7 +1,7 @@
import PathRegistry from '@/PathRegistry' import PathRegistry from '@/PathRegistry'
describe('PathRegistry', () => { describe('PathRegistry', () => {
test ('subset structure', () => { test('subset structure', () => {
const registry = new PathRegistry() const registry = new PathRegistry()
const paths = path => Array.from(registry.getSubset(path).paths()) const paths = path => Array.from(registry.getSubset(path).paths())