refactor!: FormularioForm - renamed prop errors to fieldsErrors, some internal renamings; tests semantic improvements
This commit is contained in:
parent
e55ea0c410
commit
4e05844e73
@ -94,16 +94,16 @@ export default class FormularioField extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Determines if this formulario element is v-modeled or not.
|
* Determines if this formulario element is v-modeled or not.
|
||||||
*/
|
*/
|
||||||
get hasModel (): boolean {
|
public get hasModel (): boolean {
|
||||||
return has(this.$options.propsData || {}, 'value')
|
return has(this.$options.propsData || {}, 'value')
|
||||||
}
|
}
|
||||||
|
|
||||||
private get model (): unknown {
|
public get model (): unknown {
|
||||||
const model = this.hasModel ? 'value' : 'proxy'
|
const model = this.hasModel ? 'value' : 'proxy'
|
||||||
return this.modelGetConverter(this[model])
|
return this.modelGetConverter(this[model])
|
||||||
}
|
}
|
||||||
|
|
||||||
private set model (value: unknown) {
|
public set model (value: unknown) {
|
||||||
value = this.modelSetConverter(value, this.proxy)
|
value = this.modelSetConverter(value, this.proxy)
|
||||||
|
|
||||||
if (!shallowEquals(value, this.proxy)) {
|
if (!shallowEquals(value, this.proxy)) {
|
||||||
@ -113,7 +113,7 @@ export default class FormularioField extends Vue {
|
|||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
|
|
||||||
if (typeof this.__FormularioForm_set === 'function') {
|
if (typeof this.__FormularioForm_set === 'function') {
|
||||||
this.__FormularioForm_set(this.context.name, value)
|
this.__FormularioForm_set(this.fullPath, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ export default class FormularioField extends Vue {
|
|||||||
*/
|
*/
|
||||||
setErrors (errors: string[]): void {
|
setErrors (errors: string[]): void {
|
||||||
if (!this.errorsDisabled) {
|
if (!this.errorsDisabled) {
|
||||||
this.localErrors = arrayify(errors)
|
this.localErrors = arrayify(errors) as string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="onFormSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<slot :errors="mergedFormErrors" />
|
<slot :errors="formErrorsComputed" />
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -38,9 +38,9 @@ export default class FormularioForm extends Vue {
|
|||||||
@Model('input', { default: () => ({}) })
|
@Model('input', { default: () => ({}) })
|
||||||
public readonly state!: Record<string, unknown>
|
public readonly state!: Record<string, unknown>
|
||||||
|
|
||||||
// Errors record, describing state validation errors of whole form
|
// Describes validation errors of whole form
|
||||||
@Prop({ default: () => ({}) }) readonly errors!: Record<string, string[]>
|
@Prop({ default: () => ({}) }) readonly fieldsErrors!: Record<string, string[]>
|
||||||
// Form errors only used on FormularioForm default slot
|
// Only used on FormularioForm default slot
|
||||||
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
||||||
|
|
||||||
public proxy: Record<string, unknown> = {}
|
public proxy: Record<string, unknown> = {}
|
||||||
@ -48,58 +48,57 @@ export default class FormularioForm extends Vue {
|
|||||||
private registry: PathRegistry<FormularioField> = new PathRegistry()
|
private registry: PathRegistry<FormularioField> = new PathRegistry()
|
||||||
|
|
||||||
// Local error messages are temporal, they wiped each resetValidation call
|
// Local error messages are temporal, they wiped each resetValidation call
|
||||||
|
private localFieldsErrors: Record<string, string[]> = {}
|
||||||
private localFormErrors: string[] = []
|
private localFormErrors: string[] = []
|
||||||
private localFieldErrors: Record<string, string[]> = {}
|
|
||||||
|
|
||||||
get initialValues (): Record<string, unknown> {
|
private get hasModel (): boolean {
|
||||||
|
return has(this.$options.propsData || {}, 'state')
|
||||||
|
}
|
||||||
|
|
||||||
|
private get modelIsDefined (): boolean {
|
||||||
|
return this.state && typeof this.state === 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
private get modelCopy (): Record<string, unknown> {
|
||||||
if (this.hasModel && typeof this.state === 'object') {
|
if (this.hasModel && typeof this.state === 'object') {
|
||||||
// If there is a v-model on the form/group, use those values as first priority
|
|
||||||
return { ...this.state } // @todo - use a deep clone to detach reference types
|
return { ...this.state } // @todo - use a deep clone to detach reference types
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
get mergedFormErrors (): string[] {
|
private get fieldsErrorsComputed (): Record<string, string[]> {
|
||||||
|
return merge(this.fieldsErrors || {}, this.localFieldsErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get formErrorsComputed (): string[] {
|
||||||
return [...this.formErrors, ...this.localFormErrors]
|
return [...this.formErrors, ...this.localFormErrors]
|
||||||
}
|
}
|
||||||
|
|
||||||
get mergedFieldErrors (): Record<string, string[]> {
|
|
||||||
return merge(this.errors || {}, this.localFieldErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasModel (): boolean {
|
|
||||||
return has(this.$options.propsData || {}, 'state')
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasInitialValue (): boolean {
|
|
||||||
return this.state && typeof this.state === 'object'
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('state', { deep: true })
|
@Watch('state', { deep: true })
|
||||||
onStateChange (values: Record<string, unknown>): void {
|
private onStateChange (values: Record<string, unknown>): void {
|
||||||
if (this.hasModel && values && typeof values === 'object') {
|
if (this.hasModel && values && typeof values === 'object') {
|
||||||
this.setValues(values)
|
this.setValues(values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('mergedFieldErrors', { deep: true, immediate: true })
|
@Watch('fieldsErrorsComputed', { deep: true, immediate: true })
|
||||||
onMergedFieldErrorsChange (errors: Record<string, string[]>): void {
|
private onFieldsErrorsChange (fieldsErrors: Record<string, string[]>): void {
|
||||||
this.registry.forEach((field, path) => {
|
this.registry.forEach((field, path) => {
|
||||||
field.setErrors(errors[path] || [])
|
field.setErrors(fieldsErrors[path] || [])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provide('__FormularioForm_getValue')
|
||||||
|
private getValue (): Record<string, unknown> {
|
||||||
|
return this.proxy
|
||||||
|
}
|
||||||
|
|
||||||
created (): void {
|
created (): void {
|
||||||
this.syncProxy()
|
this.syncProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide('__FormularioForm_getValue')
|
private onSubmit (): Promise<void> {
|
||||||
getFormValues (): Record<string, unknown> {
|
|
||||||
return this.proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
onFormSubmit (): Promise<void> {
|
|
||||||
return this.hasValidationErrors()
|
return this.hasValidationErrors()
|
||||||
.then(hasErrors => hasErrors ? undefined : clone(this.proxy))
|
.then(hasErrors => hasErrors ? undefined : clone(this.proxy))
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -111,30 +110,24 @@ export default class FormularioForm extends Vue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide('__FormularioForm_emitValidation')
|
|
||||||
private emitValidation (payload: ValidationEventPayload): void {
|
|
||||||
this.$emit('validation', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provide('__FormularioForm_register')
|
@Provide('__FormularioForm_register')
|
||||||
private register (path: string, field: FormularioField): void {
|
private register (path: string, field: FormularioField): void {
|
||||||
this.registry.add(path, field)
|
this.registry.add(path, field)
|
||||||
|
|
||||||
const value = getNested(this.initialValues, path)
|
const value = getNested(this.modelCopy, path)
|
||||||
|
|
||||||
if (!field.hasModel && this.hasInitialValue && value !== undefined) {
|
if (!field.hasModel && this.modelIsDefined && value !== undefined) {
|
||||||
// In the case that the form is carrying an initial value and the
|
// In the case that the form is carrying an initial value and the
|
||||||
// element is not, set it directly.
|
// element is not, set it directly.
|
||||||
// @ts-ignore
|
field.model = value
|
||||||
field.context.model = value
|
|
||||||
} else if (field.hasModel && !shallowEquals(field.proxy, value)) {
|
} else if (field.hasModel && !shallowEquals(field.proxy, value)) {
|
||||||
// In this case, the field is v-modeled or has an initial value and the
|
// In this case, the field is v-modeled or has an initial value and the
|
||||||
// form has no value or a different value, so use the field value
|
// form has no value or a different value, so use the field value
|
||||||
this.setFieldValueAndEmit(path, field.proxy)
|
this.setFieldValueAndEmit(path, field.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has(this.mergedFieldErrors, path)) {
|
if (has(this.fieldsErrorsComputed, path)) {
|
||||||
field.setErrors(this.mergedFieldErrors[path] || [])
|
field.setErrors(this.fieldsErrorsComputed[path] || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,9 +141,14 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncProxy (): void {
|
@Provide('__FormularioForm_emitValidation')
|
||||||
if (this.hasInitialValue) {
|
private emitValidation (payload: ValidationEventPayload): void {
|
||||||
this.proxy = this.initialValues
|
this.$emit('validation', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncProxy (): void {
|
||||||
|
if (this.modelIsDefined) {
|
||||||
|
this.proxy = this.modelCopy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +175,7 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shallowEquals(newValue, field.proxy)) {
|
if (!shallowEquals(newValue, field.proxy)) {
|
||||||
field.context.model = newValue
|
field.model = newValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -205,9 +203,9 @@ export default class FormularioForm extends Vue {
|
|||||||
this.$emit('input', { ...this.proxy })
|
this.$emit('input', { ...this.proxy })
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
setErrors ({ fieldsErrors, formErrors }: { fieldsErrors?: Record<string, string[]>; formErrors?: string[] }): void {
|
||||||
|
this.localFieldsErrors = fieldsErrors || {}
|
||||||
this.localFormErrors = formErrors || []
|
this.localFormErrors = formErrors || []
|
||||||
this.localFieldErrors = inputErrors || {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidationErrors (): Promise<boolean> {
|
hasValidationErrors (): Promise<boolean> {
|
||||||
@ -218,8 +216,8 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetValidation (): void {
|
resetValidation (): void {
|
||||||
|
this.localFieldsErrors = {}
|
||||||
this.localFormErrors = []
|
this.localFormErrors = []
|
||||||
this.localFieldErrors = {}
|
|
||||||
this.registry.forEach((field: FormularioField) => {
|
this.registry.forEach((field: FormularioField) => {
|
||||||
field.resetValidation()
|
field.resetValidation()
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,7 @@ import FormularioForm from '@/FormularioForm.vue'
|
|||||||
Vue.use(Formulario)
|
Vue.use(Formulario)
|
||||||
|
|
||||||
describe('FormularioFieldGroup', () => {
|
describe('FormularioFieldGroup', () => {
|
||||||
it('Grouped fields to be set', async () => {
|
test('grouped fields to be set', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
@ -36,7 +36,7 @@ describe('FormularioFieldGroup', () => {
|
|||||||
expect(emitted['submit']).toEqual([[{ group: { text: 'test' } }]])
|
expect(emitted['submit']).toEqual([[{ group: { text: 'test' } }]])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Grouped fields to be got', async () => {
|
test('grouped fields to be got', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: {
|
propsData: {
|
||||||
state: {
|
state: {
|
||||||
@ -57,11 +57,11 @@ describe('FormularioFieldGroup', () => {
|
|||||||
expect(wrapper.find('input[type="text"]').element['value']).toBe('Group text')
|
expect(wrapper.find('input[type="text"]').element['value']).toBe('Group text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Data reactive with grouped fields', async () => {
|
test('data reactive with grouped fields', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
data: () => ({ values: {} }),
|
data: () => ({ values: {} }),
|
||||||
template: `
|
template: `
|
||||||
<FormularioForm name="form" v-model="values">
|
<FormularioForm v-model="values">
|
||||||
<FormularioFieldGroup name="group">
|
<FormularioFieldGroup name="group">
|
||||||
<FormularioField name="text" v-slot="{ context }">
|
<FormularioField name="text" v-slot="{ context }">
|
||||||
<input type="text" v-model="context.model">
|
<input type="text" v-model="context.model">
|
||||||
@ -71,22 +71,23 @@ describe('FormularioFieldGroup', () => {
|
|||||||
</FormularioForm>
|
</FormularioForm>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('')
|
expect(wrapper.find('span').text()).toBe('')
|
||||||
|
|
||||||
wrapper.find('input[type="text"]').setValue('test')
|
wrapper.find('input[type="text"]').setValue('test')
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('test')
|
expect(wrapper.find('span').text()).toBe('test')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Errors are set for grouped fields', async () => {
|
test('errors are set for grouped fields', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: {
|
propsData: { fieldsErrors: { 'address.street': 'Test error' } },
|
||||||
state: {},
|
|
||||||
errors: { 'group.text': 'Test error' },
|
|
||||||
},
|
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioFieldGroup name="group">
|
<FormularioFieldGroup name="address">
|
||||||
<FormularioField ref="input" name="text" v-slot="{ context }">
|
<FormularioField ref="input" name="street" v-slot="{ context }">
|
||||||
<span v-for="error in context.errors">{{ error }}</span>
|
<span v-for="error in context.errors">{{ error }}</span>
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
</FormularioFieldGroup>
|
</FormularioFieldGroup>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import flushPromises from 'flush-promises'
|
import flushPromises from 'flush-promises'
|
||||||
|
|
||||||
import Formulario from '@/index.ts'
|
import Formulario from '@/index.ts'
|
||||||
import FormularioForm from '@/FormularioForm.vue'
|
import FormularioForm from '@/FormularioForm.vue'
|
||||||
|
|
||||||
@ -13,32 +15,22 @@ Vue.use(Formulario, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('FormularioForm', () => {
|
describe('FormularioForm', () => {
|
||||||
it('render a form DOM element', () => {
|
test('renders a form DOM element', () => {
|
||||||
const wrapper = mount(FormularioForm)
|
const wrapper = mount(FormularioForm)
|
||||||
expect(wrapper.find('form').exists()).toBe(true)
|
expect(wrapper.find('form').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts a default slot', () => {
|
test('accepts a default slot', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: {
|
slots: {
|
||||||
default: '<div class="default-slot-item" />'
|
default: '<div data-default />'
|
||||||
}
|
},
|
||||||
})
|
|
||||||
expect(wrapper.find('form div.default-slot-item').exists()).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Intercepts submit event', () => {
|
expect(wrapper.find('form [data-default]').exists()).toBe(true)
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
slots: {
|
|
||||||
default: '<button type="submit" />'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const spy = jest.spyOn(wrapper.vm, 'onFormSubmit')
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can set a field’s initial value', async () => {
|
test('can set a field’s initial value', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { state: { test: 'Has initial value' } },
|
propsData: { state: { test: 'Has initial value' } },
|
||||||
slots: {
|
slots: {
|
||||||
@ -49,131 +41,123 @@ describe('FormularioForm', () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('input').element['value']).toBe('Has initial value')
|
expect(wrapper.find('input').element['value']).toBe('Has initial value')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Lets individual fields override form initial value', () => {
|
describe('emits input event', () => {
|
||||||
|
test('when individual fields contain a populated value', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { state: { test: 'has initial value' } },
|
propsData: { state: { field: 'initial' } },
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: '<FormularioField name="field" value="populated" />'
|
||||||
<FormularioField v-slot="{ context }" name="test" value="123">
|
|
||||||
<input v-model="context.model" type="text">
|
|
||||||
</FormularioField>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(wrapper.find('input').element['value']).toBe('123')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Lets fields set form initial value with value prop', () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
data: () => ({ values: {} }),
|
|
||||||
template: `
|
|
||||||
<FormularioForm v-model="values">
|
|
||||||
<FormularioField name="test" value="123" />
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
expect(wrapper.vm['values']).toEqual({ test: '123' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Receives updates to form model when individual fields are edited', () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
data: () => ({ values: { test: '' } }),
|
|
||||||
template: `
|
|
||||||
<FormularioForm v-model="values">
|
|
||||||
<FormularioField v-slot="{ context }" name="test" >
|
|
||||||
<input v-model="context.model" type="text">
|
|
||||||
</FormularioField>
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
wrapper.find('input').setValue('Edited value')
|
|
||||||
expect(wrapper.vm['values']).toEqual({ test: 'Edited value' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Field data updates when it is type of date', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
data: () => ({ formValues: { date: new Date(123) } }),
|
|
||||||
template: `
|
|
||||||
<FormularioForm v-model="formValues" ref="form">
|
|
||||||
<FormularioField v-slot="{ context }" name="date" >
|
|
||||||
<span v-if="context.model">{{ context.model.getTime() }}</span>
|
|
||||||
</FormularioField>
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('123')
|
|
||||||
|
|
||||||
wrapper.setData({ formValues: { date: new Date(234) } })
|
|
||||||
await flushPromises()
|
|
||||||
|
|
||||||
expect(wrapper.find('span').text()).toBe('234')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Updates initial form values when input contains a populated v-model', async () => {
|
|
||||||
const wrapper = mount({
|
|
||||||
data: () => ({
|
|
||||||
formValues: { test: '' },
|
|
||||||
fieldValue: '123',
|
|
||||||
}),
|
|
||||||
template: `
|
|
||||||
<FormularioForm v-model="formValues">
|
|
||||||
<FormularioField name="test" v-model="fieldValue" />
|
|
||||||
</FormularioForm>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.vm['formValues']).toEqual({ test: '123' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Replacement test for the above test - not quite as good of a test.
|
|
||||||
it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
|
|
||||||
const wrapper = mount(FormularioForm, {
|
|
||||||
propsData: {
|
|
||||||
state: { test: 'Initial' },
|
|
||||||
},
|
|
||||||
slots: {
|
|
||||||
default: '<FormularioField name="test" value="Overrides" />'
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
const emitted = wrapper.emitted('input')
|
const emitted = wrapper.emitted('input')
|
||||||
|
|
||||||
expect(emitted).toBeTruthy()
|
expect(emitted).toBeTruthy()
|
||||||
expect(emitted[emitted.length - 1]).toEqual([{ test: 'Overrides' }])
|
expect(emitted[emitted.length - 1]).toEqual([{ field: 'populated' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates an inputs value when the form v-model is modified', async () => {
|
test('when individual fields are edited', () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount(FormularioForm, {
|
||||||
data: () => ({ values: { test: 'abcd' } }),
|
propsData: { state: { field: 'initial' } },
|
||||||
template: `
|
slots: {
|
||||||
<FormularioForm v-model="values">
|
default: `
|
||||||
<FormularioField v-slot="{ context }" name="test" >
|
<FormularioField v-slot="{ context }" name="field" >
|
||||||
<input v-model="context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
</FormularioForm>
|
`,
|
||||||
`
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.values = { test: '1234' }
|
wrapper.find('input').setValue('updated')
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted('input')
|
||||||
|
|
||||||
|
expect(emitted).toBeTruthy()
|
||||||
|
expect(emitted[emitted.length - 1]).toEqual([{ field: 'updated' }])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updates a field when the form v-model is modified', async () => {
|
||||||
|
const wrapper = mount(FormularioForm, {
|
||||||
|
propsData: { state: { field: 'initial' } },
|
||||||
|
slots: {
|
||||||
|
default: `
|
||||||
|
<FormularioField v-slot="{ context }" name="field">
|
||||||
|
<input v-model="context.model" type="text">
|
||||||
|
</FormularioField>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const input = wrapper.find('input')
|
||||||
|
|
||||||
|
expect(input).toBeTruthy()
|
||||||
|
expect(input.element['value']).toBe('initial')
|
||||||
|
|
||||||
|
wrapper.setProps({ state: { field: 'updated' } })
|
||||||
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
|
expect(input.element['value']).toBe('updated')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updates a field when it is an instance of Date', async () => {
|
||||||
|
const dateA = new Date('1970-01-01')
|
||||||
|
const dateB = new Date()
|
||||||
|
|
||||||
|
const wrapper = mount(FormularioForm,{
|
||||||
|
propsData: { state: { date: dateA } },
|
||||||
|
scopedSlots: {
|
||||||
|
default: `
|
||||||
|
<FormularioField v-slot="{ context }" name="date">
|
||||||
|
<span v-if="context.model">{{ context.model.toISOString() }}</span>
|
||||||
|
</FormularioField>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('span').text()).toBe(dateA.toISOString())
|
||||||
|
|
||||||
|
wrapper.setProps({ state: { date: dateB } })
|
||||||
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('span').text()).toBe(dateB.toISOString())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolves submitted form values to an object', async () => {
|
||||||
|
const wrapper = mount(FormularioForm, {
|
||||||
|
slots: {
|
||||||
|
default: '<FormularioField name="fieldName" validation="required" value="Justin" />'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const input = wrapper.find('input[type="text"]')
|
expect(wrapper.emitted('submit')).toEqual([
|
||||||
|
[{ fieldName: 'Justin' }],
|
||||||
expect(input).toBeTruthy()
|
])
|
||||||
expect(input.element['value']).toBe('1234')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Resolves hasValidationErrors to true', async () => {
|
test('resolves hasValidationErrors to true', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
slots: { default: '<FormularioField name="fieldName" validation="required" />' }
|
slots: {
|
||||||
|
default: '<FormularioField name="fieldName" validation="required" />',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const emitted = wrapper.emitted()
|
const emitted = wrapper.emitted()
|
||||||
@ -182,89 +166,102 @@ describe('FormularioForm', () => {
|
|||||||
expect(emitted['error'].length).toBe(1)
|
expect(emitted['error'].length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Resolves submitted form values to an object', async () => {
|
describe('allows setting fields errors', () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
/**
|
||||||
slots: { default: '<FormularioField name="fieldName" validation="required" value="Justin" />' }
|
* @param props
|
||||||
})
|
* @return {Wrapper<FormularioForm>}
|
||||||
wrapper.find('form').trigger('submit')
|
*/
|
||||||
await flushPromises()
|
const createWrapper = (props = {}) => mount(FormularioForm, {
|
||||||
|
propsData: props,
|
||||||
const emitted = wrapper.emitted()
|
scopedSlots: {
|
||||||
|
default: '<div><div v-for="error in props.errors" data-error /></div>',
|
||||||
expect(emitted['submit']).toBeTruthy()
|
},
|
||||||
expect(emitted['submit'].length).toBe(1)
|
|
||||||
expect(emitted['submit'][0]).toEqual([{ fieldName: 'Justin' }])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Receives a form-errors prop and displays it', async () => {
|
test('via prop', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = createWrapper({ formErrors: ['first', 'second'] })
|
||||||
propsData: { formErrors: ['first', 'second'] },
|
|
||||||
})
|
expect(wrapper.findAll('[data-error]').length).toBe(2)
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.vm.mergedFormErrors.length).toBe(2)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Aggregates form-errors prop with form-named errors', async () => {
|
test('manually with setErrors()', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = createWrapper({ formErrors: ['first', 'second'] })
|
||||||
propsData: { formErrors: ['first', 'second'] }
|
|
||||||
})
|
|
||||||
wrapper.vm.setErrors({ formErrors: ['third'] })
|
wrapper.vm.setErrors({ formErrors: ['third'] })
|
||||||
|
|
||||||
await flushPromises()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
expect(Object.keys(wrapper.vm.mergedFormErrors).length).toBe(3)
|
expect(wrapper.findAll('[data-error]').length).toBe(3)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays field errors on inputs with errors prop', async () => {
|
test('displays field errors on inputs with errors prop', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = mount(FormularioForm, {
|
||||||
propsData: { errors: { fieldWithErrors: ['This field has an error'] }},
|
propsData: { fieldsErrors: { field: ['This field has an error'] }},
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioField v-slot="{ context }" name="fieldWithErrors">
|
<FormularioField v-slot="{ context }" name="field">
|
||||||
<span v-for="error in context.errors">{{ error }}</span>
|
<span v-for="error in context.errors">{{ error }}</span>
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
`
|
`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(true)
|
expect(wrapper.find('span').exists()).toBe(true)
|
||||||
expect(wrapper.find('span').text()).toEqual('This field has an error')
|
expect(wrapper.find('span').text()).toEqual('This field has an error')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Is able to display multiple errors on multiple elements', async () => {
|
describe('allows setting fields errors', () => {
|
||||||
const errors = { inputA: ['first'], inputB: ['first', 'second']}
|
/**
|
||||||
const wrapper = mount(FormularioForm, { propsData: { errors } })
|
* @param props
|
||||||
|
* @return {Wrapper<FormularioForm>}
|
||||||
|
*/
|
||||||
|
const createWrapper = (props = {}) => mount(FormularioForm, {
|
||||||
|
propsData: props,
|
||||||
|
slots: {
|
||||||
|
default: `
|
||||||
|
<div>
|
||||||
|
<FormularioField v-slot="{ context }" name="fieldA">
|
||||||
|
<div v-for="error in context.errors" data-error-a>{{ error }}</div>
|
||||||
|
</FormularioField>
|
||||||
|
|
||||||
await wrapper.vm.$nextTick()
|
<FormularioField v-slot="{ context }" name="fieldB">
|
||||||
|
<div v-for="error in context.errors" data-error-b>{{ error }}</div>
|
||||||
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(2)
|
</FormularioField>
|
||||||
expect(wrapper.vm.mergedFieldErrors.inputA.length).toBe(1)
|
</div>
|
||||||
expect(wrapper.vm.mergedFieldErrors.inputB.length).toBe(2)
|
`,
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set multiple field errors with setErrors()', async () => {
|
|
||||||
const wrapper = mount(FormularioForm)
|
|
||||||
|
|
||||||
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(0)
|
|
||||||
|
|
||||||
wrapper.vm.setErrors({
|
|
||||||
inputErrors: {
|
|
||||||
inputA: ['first'],
|
|
||||||
inputB: ['first', 'second'],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.$nextTick()
|
test('via prop', async () => {
|
||||||
await flushPromises()
|
const wrapper = createWrapper({
|
||||||
|
fieldsErrors: { fieldA: ['first'], fieldB: ['first', 'second']},
|
||||||
expect(Object.keys(wrapper.vm.mergedFieldErrors).length).toBe(2)
|
|
||||||
expect(wrapper.vm.mergedFieldErrors.inputA.length).toBe(1)
|
|
||||||
expect(wrapper.vm.mergedFieldErrors.inputB.length).toBe(2)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits correct validation event when no errors', async () => {
|
expect(wrapper.findAll('[data-error-a]').length).toBe(1)
|
||||||
const wrapper = mount(FormularioForm, {
|
expect(wrapper.findAll('[data-error-b]').length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('manually with setErrors()', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
|
||||||
|
expect(wrapper.findAll('[data-error-a]').length).toBe(0)
|
||||||
|
expect(wrapper.findAll('[data-error-b]').length).toBe(0)
|
||||||
|
|
||||||
|
wrapper.vm.setErrors({ fieldsErrors: { fieldA: ['first'], fieldB: ['first', 'second'] } })
|
||||||
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.findAll('[data-error-a]').length).toBe(1)
|
||||||
|
expect(wrapper.findAll('[data-error-b]').length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('emits correct validation event', () => {
|
||||||
|
/**
|
||||||
|
* @return {Wrapper<FormularioForm>}
|
||||||
|
*/
|
||||||
|
const createWrapper = () => mount(FormularioForm, {
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<FormularioField v-slot="{ context }" name="foo" validation="required|in:foo">
|
<FormularioField v-slot="{ context }" name="foo" validation="required|in:foo">
|
||||||
@ -272,8 +269,12 @@ describe('FormularioForm', () => {
|
|||||||
</FormularioField>
|
</FormularioField>
|
||||||
<FormularioField name="bar" validation="required" />
|
<FormularioField name="bar" validation="required" />
|
||||||
`,
|
`,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('when no errors', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
|
||||||
wrapper.find('input[type="text"]').setValue('foo')
|
wrapper.find('input[type="text"]').setValue('foo')
|
||||||
wrapper.find('input[type="text"]').trigger('blur')
|
wrapper.find('input[type="text"]').trigger('blur')
|
||||||
|
|
||||||
@ -286,111 +287,91 @@ describe('FormularioForm', () => {
|
|||||||
}]])
|
}]])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Emits correct validation event on entry', async () => {
|
test('on entry', async () => {
|
||||||
const wrapper = mount(FormularioForm, {
|
const wrapper = createWrapper()
|
||||||
slots: { default: `
|
|
||||||
<FormularioField
|
|
||||||
v-slot="{ context }"
|
|
||||||
name="firstField"
|
|
||||||
validation="required|in:foo"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="context.model"
|
|
||||||
type="text"
|
|
||||||
@blur="context.runValidation()"
|
|
||||||
>
|
|
||||||
</FormularioField>
|
|
||||||
<FormularioField
|
|
||||||
name="secondField"
|
|
||||||
validation="required"
|
|
||||||
/>
|
|
||||||
` }
|
|
||||||
})
|
|
||||||
wrapper.find('input[type="text"]').setValue('bar')
|
wrapper.find('input[type="text"]').setValue('bar')
|
||||||
wrapper.find('input[type="text"]').trigger('blur')
|
wrapper.find('input[type="text"]').trigger('blur')
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.emitted('validation')).toBeTruthy()
|
expect(wrapper.emitted('validation')).toBeTruthy()
|
||||||
expect(wrapper.emitted('validation').length).toBe(1)
|
expect(wrapper.emitted('validation')).toEqual([[ {
|
||||||
expect(wrapper.emitted('validation')[0][0]).toEqual({
|
name: 'foo',
|
||||||
name: 'firstField',
|
|
||||||
violations: [ {
|
violations: [ {
|
||||||
rule: expect.any(String),
|
rule: expect.any(String),
|
||||||
args: ['foo'],
|
args: ['foo'],
|
||||||
context: {
|
context: {
|
||||||
value: 'bar',
|
value: 'bar',
|
||||||
formValues: expect.any(Object),
|
formValues: expect.any(Object),
|
||||||
name: 'firstField',
|
name: 'foo',
|
||||||
},
|
},
|
||||||
message: expect.any(String),
|
message: expect.any(String),
|
||||||
} ],
|
} ],
|
||||||
|
} ]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Allows resetting a form, wiping validation.', async () => {
|
test('allows resetting form validation', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount(FormularioForm, {
|
||||||
data: () => ({ values: {} }),
|
slots: {
|
||||||
template: `
|
default: `
|
||||||
<FormularioForm
|
<div>
|
||||||
v-model="values"
|
|
||||||
name="login"
|
|
||||||
ref="form"
|
|
||||||
>
|
|
||||||
<FormularioField v-slot="{ context }" name="username" validation="required">
|
<FormularioField v-slot="{ context }" name="username" validation="required">
|
||||||
<input v-model="context.model" type="text">
|
<input v-model="context.model" type="text">
|
||||||
|
<div v-for="error in context.allErrors" data-username-error />
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
|
|
||||||
<FormularioField v-slot="{ context }" name="password" validation="required|min:4,length">
|
<FormularioField v-slot="{ context }" name="password" validation="required|min:4,length">
|
||||||
<input v-model="context.model" type="password">
|
<input v-model="context.model" type="password" @blur="context.runValidation()">
|
||||||
|
<div v-for="error in context.allErrors" data-password-error />
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
</FormularioForm>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const password = wrapper.find('input[type="password"]')
|
const password = wrapper.find('input[type="password"]')
|
||||||
|
|
||||||
password.setValue('foo')
|
password.setValue('foo')
|
||||||
|
password.trigger('input')
|
||||||
password.trigger('blur')
|
password.trigger('blur')
|
||||||
|
|
||||||
wrapper.find('form').trigger('submit')
|
|
||||||
wrapper.vm.$refs.form.setErrors({ inputErrors: { username: ['Failed'] } })
|
|
||||||
|
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// First make sure we caught the errors
|
wrapper.vm.setErrors({ fieldsErrors: { username: ['required'] } })
|
||||||
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(1)
|
|
||||||
wrapper.vm.$refs.form.resetValidation()
|
|
||||||
|
|
||||||
await flushPromises()
|
await Vue.nextTick()
|
||||||
|
|
||||||
expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0)
|
expect(wrapper.findAll('[data-username-error]').length).toBe(1)
|
||||||
|
expect(wrapper.findAll('[data-password-error]').length).toBe(1)
|
||||||
|
|
||||||
|
wrapper.vm.resetValidation()
|
||||||
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.findAll('[data-username-error]').length).toBe(0)
|
||||||
|
expect(wrapper.findAll('[data-password-error]').length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Local errors resetted when errors prop cleared', async () => {
|
test('local errors are reset when errors prop cleared', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount(FormularioForm, {
|
||||||
data: () => ({ values: {}, errors: { input: ['failure'] } }),
|
propsData: { fieldsErrors: { input: ['failure'] } },
|
||||||
template: `
|
slots: {
|
||||||
<FormularioForm
|
default: `
|
||||||
v-model="values"
|
<FormularioField v-slot="{ context }" name="input">
|
||||||
:errors="errors"
|
|
||||||
ref="form"
|
|
||||||
>
|
|
||||||
<FormularioField
|
|
||||||
v-slot="{ context }"
|
|
||||||
name="input"
|
|
||||||
ref="form"
|
|
||||||
>
|
|
||||||
<span v-for="error in context.allErrors">{{ error.message }}</span>
|
<span v-for="error in context.allErrors">{{ error.message }}</span>
|
||||||
</FormularioField>
|
</FormularioField>
|
||||||
</FormularioForm>
|
`,
|
||||||
`
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.find('span').exists()).toBe(true)
|
expect(wrapper.find('span').exists()).toBe(true)
|
||||||
|
|
||||||
wrapper.vm.errors = {}
|
wrapper.setProps({ fieldsErrors: {} })
|
||||||
await flushPromises()
|
|
||||||
|
await Vue.nextTick()
|
||||||
|
|
||||||
expect(wrapper.find('span').exists()).toBe(false)
|
expect(wrapper.find('span').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user