diff --git a/.github/workflows/pr-run-tests.yml b/.github/workflows/pr-run-tests.yml new file mode 100644 index 0000000..cd3505f --- /dev/null +++ b/.github/workflows/pr-run-tests.yml @@ -0,0 +1,36 @@ +name: Validate tests for pull request + +on: + pull_request: + types: [ opened, synchronize, reopened ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - name: Install dependencies + run: pnpm install + - name: Run tests + run: pnpm run test \ No newline at end of file diff --git a/apps/docs/.vitepress/config.js b/apps/docs/.vitepress/config.mts similarity index 96% rename from apps/docs/.vitepress/config.js rename to apps/docs/.vitepress/config.mts index a7ba062..ba7b598 100644 --- a/apps/docs/.vitepress/config.js +++ b/apps/docs/.vitepress/config.mts @@ -82,7 +82,8 @@ export default defineConfig({ ], socialLinks: [ - { icon: 'github', link: 'https://github.com/kevinkosterr/vue3-form-generator' } + { icon: 'github', link: 'https://github.com/kevinkosterr/vue3-form-generator' }, + { icon: 'npm', link: 'https://www.npmjs.com/package/@kevinkosterr/vue3-form-generator' } ] }, vite: { diff --git a/src/FormGenerator.vue b/src/FormGenerator.vue index 0bab1af..19e0934 100644 --- a/src/FormGenerator.vue +++ b/src/FormGenerator.vue @@ -1,6 +1,45 @@ + + - - \ No newline at end of file + \ No newline at end of file diff --git a/src/FormGroup.vue b/src/FormGroup.vue index 74bcf7f..4efbd2e 100644 --- a/src/FormGroup.vue +++ b/src/FormGroup.vue @@ -1,53 +1,5 @@ - - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/directives/onClickOutside.ts b/src/directives/onClickOutside.ts index eb4f072..4adefd0 100644 --- a/src/directives/onClickOutside.ts +++ b/src/directives/onClickOutside.ts @@ -1,6 +1,6 @@ import { Directive, DirectiveBinding } from 'vue' -const onClickOutside: Directive = { +const onClickOutside: Directive void> = { beforeMount(el: HTMLElement, binding: DirectiveBinding): void { el.clickOutsideEvent = (event: Event) => { if (!(el === event.target || el.contains(event.target))) { diff --git a/src/fields/core/FieldButton.vue b/src/fields/core/FieldButton.vue index c181003..3ec00ef 100644 --- a/src/fields/core/FieldButton.vue +++ b/src/fields/core/FieldButton.vue @@ -1,20 +1,27 @@ \ No newline at end of file diff --git a/src/fields/core/FieldCheckbox.vue b/src/fields/core/FieldCheckbox.vue index e603ea7..1de35a3 100644 --- a/src/fields/core/FieldCheckbox.vue +++ b/src/fields/core/FieldCheckbox.vue @@ -28,7 +28,7 @@ const props = defineProps>() const { field, model }: FieldPropRefs = toRefs(props) const { currentModelValue } = useFormModel(model.value, field.value) -const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value) const { errors, validate } = useFieldValidate( model.value, field.value, @@ -50,5 +50,5 @@ const onFieldValueChanged = (event: Event) => { emits('onInput', target.checked) } -defineExpose({ hint, noLabel: true, errors }) - \ No newline at end of file +defineExpose({ hint, noLabel: true, errors, isVisible }) + diff --git a/src/fields/core/FieldChecklist.vue b/src/fields/core/FieldChecklist.vue index 03377f7..e6df313 100644 --- a/src/fields/core/FieldChecklist.vue +++ b/src/fields/core/FieldChecklist.vue @@ -34,7 +34,7 @@ const emits = defineEmits(useFieldEmits()) const props = defineProps>() const { field, model }: FieldPropRefs = toRefs(props) -const { hint } = useFieldAttributes(model.value, field.value) +const { hint, isVisible } = useFieldAttributes(model.value, field.value) const { currentModelValue }: { currentModelValue: Ref } = useFormModel(model.value, field.value) const { validate, errors } = useFieldValidate(model.value, field.value) @@ -63,5 +63,5 @@ const onFieldValueChanged = (event: Event) => { }) } -defineExpose({ hint, errors }) - \ No newline at end of file +defineExpose({ hint, errors, isVisible }) + diff --git a/src/fields/core/FieldMask.vue b/src/fields/core/FieldMask.vue index b1861a1..c645bbb 100644 --- a/src/fields/core/FieldMask.vue +++ b/src/fields/core/FieldMask.vue @@ -38,7 +38,7 @@ const maskOptions: ComputedRef = computed(() => { }) const { currentModelValue } = useFormModel(model.value, field.value) -const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value) const { errors, validate } = useFieldValidate( model.value, field.value, @@ -76,5 +76,5 @@ onBeforeMount(() => { } }) -defineExpose({ unmaskedValue, hint, errors }) - \ No newline at end of file +defineExpose({ unmaskedValue, hint, errors, isVisible }) + diff --git a/src/fields/core/FieldNumber.vue b/src/fields/core/FieldNumber.vue index 2ae7a3b..7e65857 100644 --- a/src/fields/core/FieldNumber.vue +++ b/src/fields/core/FieldNumber.vue @@ -31,7 +31,7 @@ const emits = defineEmits(useFieldEmits()) const { field, model }: FieldPropRefs = toRefs(props) -const { isDisabled, isRequired, hint } = useFieldAttributes(model.value, field.value) +const { isDisabled, isRequired, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) const { errors, validate } = useFieldValidate( model.value, @@ -60,5 +60,5 @@ const onFieldValueChanged = (event: Event) => { emits('onInput', parseFloat(target.value)) } -defineExpose({ hint, errors }) - \ No newline at end of file +defineExpose({ hint, errors, isVisible }) + diff --git a/src/fields/core/FieldObject.vue b/src/fields/core/FieldObject.vue index d69e773..9c1116d 100644 --- a/src/fields/core/FieldObject.vue +++ b/src/fields/core/FieldObject.vue @@ -13,7 +13,7 @@ \ No newline at end of file +defineExpose({ hasErrors, isVisible }) + diff --git a/src/fields/core/FieldPassword.vue b/src/fields/core/FieldPassword.vue index c9bc1c3..0c3a98a 100644 --- a/src/fields/core/FieldPassword.vue +++ b/src/fields/core/FieldPassword.vue @@ -32,7 +32,7 @@ const props = defineProps>() const emits = defineEmits(useFieldEmits()) const { model, field }: FieldPropRefs = toRefs(props) -const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) const { errors, validate } = useFieldValidate( @@ -79,5 +79,5 @@ const onBlur = () => { }) } -defineExpose({ hint, errors }) - \ No newline at end of file +defineExpose({ hint, errors, isVisible }) + diff --git a/src/fields/core/FieldRadio.vue b/src/fields/core/FieldRadio.vue index d5937a0..cdbb1ae 100644 --- a/src/fields/core/FieldRadio.vue +++ b/src/fields/core/FieldRadio.vue @@ -28,7 +28,7 @@ const props = defineProps>() const emits = defineEmits(useFieldEmits()) const { field, model }: FieldPropRefs = toRefs(props) -const { isRequired, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) const getFieldId = (optionName: string) => `${field.value.name}_${optionName}` @@ -37,5 +37,5 @@ const onFieldValueChanged = (event: Event) => { emits('onInput', (event.target as HTMLInputElement).value) } -defineExpose({ hint }) - \ No newline at end of file +defineExpose({ hint, isVisible }) + diff --git a/src/fields/core/FieldReset.vue b/src/fields/core/FieldReset.vue index 3df89a1..93ed816 100644 --- a/src/fields/core/FieldReset.vue +++ b/src/fields/core/FieldReset.vue @@ -1,14 +1,22 @@ \ No newline at end of file +defineExpose({ noLabel: true, isVisible }) + diff --git a/src/fields/core/FieldSelect.vue b/src/fields/core/FieldSelect.vue index 87a095f..967f837 100644 --- a/src/fields/core/FieldSelect.vue +++ b/src/fields/core/FieldSelect.vue @@ -72,6 +72,7 @@ import type { FieldProps, FieldPropRefs, SelectField } from '@/resources/types/f import { useFieldAttributes, useFieldEmits, + useFieldValidate, useFormModel } from '@/composables' import { FieldOption } from '@/resources/types/fieldAttributes' @@ -82,7 +83,8 @@ const { controlLeft, metaLeft } = useMagicKeys() const isOpened: Ref = ref(false) const { field, model }: FieldPropRefs = toRefs(props) -const { hint } = useFieldAttributes(model.value, field.value) +const { hint, isVisible } = useFieldAttributes(model.value, field.value) +const { errors, validate } = useFieldValidate(model.value, field.value) /** Names of the selected values */ const selectedNames: ComputedRef = computed(() => { @@ -126,6 +128,7 @@ function handleClickOutside (event: Event) { } function selectOption (option: FieldOption) { + errors.value = [] const optionSelected = isSelected(option) if (!field.value.multiple) { @@ -140,10 +143,19 @@ function selectOption (option: FieldOption) { } emits('onInput', selectedValues) - if (metaLeft.value || controlLeft.value) return } - isOpened.value = false + if (!(metaLeft.value || controlLeft.value)) { + isOpened.value = false + } + validate(currentModelValue.value).then(validationErrors => { + emits( + 'validated', + validationErrors.length === 0, + validationErrors, + field.value + ) + }) } -defineExpose({ hint }) - \ No newline at end of file +defineExpose({ hint, isVisible, errors }) + diff --git a/src/fields/core/FieldSelectNative.vue b/src/fields/core/FieldSelectNative.vue index 954d3f7..f43ce68 100644 --- a/src/fields/core/FieldSelectNative.vue +++ b/src/fields/core/FieldSelectNative.vue @@ -32,9 +32,9 @@ const emits = defineEmits(useFieldEmits()) const { field, model }: FieldPropRefs = toRefs(props) -const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) -const { validate } = useFieldValidate(model.value, field.value) +const { validate, errors } = useFieldValidate(model.value, field.value) const onBlur = () => { validate(currentModelValue.value).then((validationErrors) => { @@ -47,8 +47,9 @@ const onBlur = () => { } const onFieldValueChanged = (event: Event) => { + errors.value = [] emits('onInput', (event.target as HTMLSelectElement).value) } -defineExpose({ hint }) +defineExpose({ hint, isVisible, errors }) \ No newline at end of file diff --git a/src/fields/core/FieldSubmit.vue b/src/fields/core/FieldSubmit.vue index be487d1..303a49d 100644 --- a/src/fields/core/FieldSubmit.vue +++ b/src/fields/core/FieldSubmit.vue @@ -17,7 +17,7 @@ const props = defineProps>() const { model, field }: FieldPropRefs = toRefs(props) -const { isDisabled } = useFieldAttributes(model.value, field.value) +const { isDisabled, isVisible } = useFieldAttributes(model.value, field.value) -defineExpose({ noLabel: true }) +defineExpose({ noLabel: true, isVisible }) diff --git a/src/fields/core/FieldSwitch.vue b/src/fields/core/FieldSwitch.vue index 7fcbc01..ef01813 100644 --- a/src/fields/core/FieldSwitch.vue +++ b/src/fields/core/FieldSwitch.vue @@ -17,6 +17,7 @@ import type { SwitchField, FieldPropRefs, FieldProps } from '@/resources/types/f import { useFieldEmits, useFieldAttributes, + useFieldValidate, useFormModel } from '@/composables' @@ -26,10 +27,21 @@ const emits = defineEmits(useFieldEmits()) const { field, model }: FieldPropRefs = toRefs(props) -const { isDisabled } = useFieldAttributes(model.value, field.value) +const { isDisabled, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) +const { errors, validate } = useFieldValidate(model.value, field.value) const onFieldValueChanged = (event: Event) => { - emits('onInput', (event.target as HTMLInputElement).checked) + const target = event.target as HTMLInputElement + emits('onInput', target.checked) + validate(target.checked).then(validationErrors => { + emits('validated', + validationErrors.length === 0, + validationErrors, + field.value + ) + }) } - \ No newline at end of file + +defineExpose({ isVisible, hint, errors }) + diff --git a/src/fields/core/FieldText.vue b/src/fields/core/FieldText.vue index 714fe04..9f16bc8 100644 --- a/src/fields/core/FieldText.vue +++ b/src/fields/core/FieldText.vue @@ -6,6 +6,7 @@ :name="field.name" :required="isRequired" :disabled="isDisabled" + :readonly="isReadonly" :placeholder="field.placeholder" :autocomplete="autoCompleteState" :value="currentModelValue" @@ -27,7 +28,7 @@ const { field, model }: FieldPropRefs = toRefs(props) const autoCompleteState: ComputedRef = computed(() => field.value.autocomplete ? 'on' : 'off') const { currentModelValue } = useFormModel(model.value, field.value) -const { isRequired, isDisabled, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isReadonly, isVisible, hint } = useFieldAttributes(model.value, field.value) const { errors, validate } = useFieldValidate( model.value, field.value, @@ -51,5 +52,5 @@ const onFieldValueChanged = (event: Event) => { emits('onInput', (event.target as HTMLInputElement).value) } -defineExpose({ errors, hint }) +defineExpose({ errors, hint, isVisible }) \ No newline at end of file diff --git a/src/fields/core/FieldTextarea.vue b/src/fields/core/FieldTextarea.vue index ff0f92b..bd306c9 100644 --- a/src/fields/core/FieldTextarea.vue +++ b/src/fields/core/FieldTextarea.vue @@ -31,7 +31,7 @@ const emits = defineEmits(useFieldEmits()) const { field, model }: FieldPropRefs = toRefs(props) -const { isRequired, isDisabled, isReadonly, hint } = useFieldAttributes(model.value, field.value) +const { isRequired, isDisabled, isReadonly, isVisible, hint } = useFieldAttributes(model.value, field.value) const { currentModelValue } = useFormModel(model.value, field.value) const { validate, errors } = useFieldValidate(model.value, field.value) @@ -50,5 +50,5 @@ const onFieldValueChanged = (event: Event) => { emits('onInput', (event.target as HTMLTextAreaElement).value) } -defineExpose({ hint, errors }) - \ No newline at end of file +defineExpose({ hint, errors, isVisible }) + diff --git a/src/resources/types/field/base.ts b/src/resources/types/field/base.ts index 16f6968..33ed78b 100644 --- a/src/resources/types/field/base.ts +++ b/src/resources/types/field/base.ts @@ -22,6 +22,7 @@ export type FieldBase = { hint?: string | TDynamicAttributeStringFunction; validator?: TValidatorFunction | TValidatorFunction[], onValidated?: TOnValidatedFunction + noLabel?: boolean; } /** diff --git a/src/resources/types/field/fields.ts b/src/resources/types/field/fields.ts index 8464057..6eb651b 100644 --- a/src/resources/types/field/fields.ts +++ b/src/resources/types/field/fields.ts @@ -109,8 +109,8 @@ export interface FieldPropRefs { export interface FieldProps { id: string; - formGenerator: object; - formOptions: FormOptions; + formGenerator?: object; + formOptions?: FormOptions; field: T; model: Record; } diff --git a/src/resources/types/generic.ts b/src/resources/types/generic.ts index 969b21f..f882c24 100644 --- a/src/resources/types/generic.ts +++ b/src/resources/types/generic.ts @@ -1,6 +1,6 @@ import { TValidatorFunction } from '@/resources/types/functions' -import { Component } from 'vue' -import type { Field } from '@/resources/types/field/fields' +import type { ComponentPublicInstance, Component, Ref } from 'vue' +import type { Field, FieldProps } from '@/resources/types/field/fields' import { FormModel } from '@/resources/types/fieldAttributes' export type ValidatorMap = Record @@ -33,6 +33,13 @@ export type FormGeneratorSchema = { }, } +export type FormGroupProps = { + formOptions?: FormOptions; + model: FormModel; + field: Field; + errors?: string[]; +} + export type FormGeneratorProps = { id?: string; idPrefix?: string; @@ -42,6 +49,19 @@ export type FormGeneratorProps = { enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain' } +export type FieldExposedValues = { + // Whether the field is visible. + isVisible: Ref; + // Errors occurred during validation of this field. + errors: Ref; + // Hint displayed underneath the field. + hint?: Ref; + // If true, the field component manages the label or doesn't have a label at all. + noLabel?: boolean; +} + +export type FieldComponent = ComponentPublicInstance + export type FormOptions = { idPrefix?: string; } diff --git a/tests/_resources/utils.js b/tests/_resources/utils.js index d20fdcc..4c0cd0e 100644 --- a/tests/_resources/utils.js +++ b/tests/_resources/utils.js @@ -12,6 +12,17 @@ export function mountFormGenerator (schema, model) { return mount(FormGenerator, { props: { schema, model } }) } +/** + * Clear all emitted events from a component, used for testing different emit phases in the same + * test case. + * @param {VueWrapper} wrapper - wrapper to clear emits for. + */ +export function clearEmittedEvents (wrapper) { + Object.keys(wrapper.emitted()).forEach(key => { + wrapper.emitted()[key].length = 0 + }) +} + /** * Generate a form schema for a single field component * @param {String} name - name of the field diff --git a/tests/components/FormGenerator.spec.js b/tests/components/FormGenerator.spec.js index ff378d2..a9cac4e 100644 --- a/tests/components/FormGenerator.spec.js +++ b/tests/components/FormGenerator.spec.js @@ -1,14 +1,16 @@ import { expect, it, describe, beforeAll } from 'vitest' import { config, mount } from '@vue/test-utils' +import { generateSchemaSingleField, clearEmittedEvents } from '@test/_resources/utils.js' +import { mountFormGenerator } from '@test/_resources/utils.js' import FormGenerator from '@/FormGenerator.vue' import FieldText from '@/fields/core/FieldText.vue' import FieldTextarea from '@/fields/core/FieldTextarea.vue' import FieldSubmit from '@/fields/core/FieldSubmit.vue' -import { generateSchemaSingleField } from '@test/_resources/utils.js' +import FieldReset from '@/fields/core/FieldReset.vue' beforeAll(() => { - config.global.components = { FieldText, FieldTextarea, FieldSubmit } + config.global.components = { FieldText, FieldTextarea, FieldSubmit, FieldReset } }) const textSchema = generateSchemaSingleField( @@ -33,8 +35,20 @@ const textAreaSchema = generateSchemaSingleField( {} ) +const resetSchema = { + name: 'reset', + type: 'reset', + buttonText: 'Reset' +} + +const submitSchema = { + name: 'submit', + type: 'submit', + buttonText: 'Submit' +} + const schema = { - schema: { fields: [ ...textSchema.schema.fields, ...textAreaSchema.schema.fields ] }, + schema: { fields: [ ...textSchema.schema.fields, ...textAreaSchema.schema.fields, resetSchema, submitSchema ] }, model: { ...textSchema.model, ...textAreaSchema.model } } @@ -46,10 +60,66 @@ describe('FormGenerator', () => { }) it('Should render with a schema', async () => { - const wrapper = mount(FormGenerator, { props: { model: schema.model, schema: schema.schema } }) + const wrapper = mountFormGenerator(schema.schema, schema.model) expect(wrapper.find('form').exists()).toBeTruthy() expect(wrapper.findComponent(FieldText).exists()).toBeTruthy() expect(wrapper.findComponent(FieldTextarea).exists()).toBeTruthy() }) + it('Should have correct internals', async () => { + const wrapper = mountFormGenerator(schema.schema, schema.model) + + /** FieldElements, unexposed, contains the refs of the field components. */ + expect(wrapper.vm.fieldElements).toHaveLength(schema.schema.fields.length) + // All field elements must be of type FormGroup + wrapper.vm.fieldElements.forEach(field => { + expect(field.$.type.__name).toBe('FormGroup') + }) + + const isObject = (o) => !Array.isArray(o) && typeof o === 'object' && o !== null + expect(isObject(wrapper.vm.formOptions)).toBeTruthy() + }) + + it('Should properly update and reset model', async () => { + const wrapper = mountFormGenerator(schema.schema, schema.model) + await wrapper.vm.$nextTick() + + const textField = wrapper.findComponent(FieldText) + expect(wrapper.vm.model.textModel).toBe('') + await textField.find('input').setValue('Test update') + expect(wrapper.vm.model.textModel).toBe('Test update') + + const resetField = wrapper.findComponent(FieldReset) + await resetField.find('input').trigger('reset') + expect(wrapper.vm.model.textModel).toBe('') + }) + + it('Should properly pass emits', async () => { + const wrapper = mountFormGenerator(schema.schema, schema.model) + await wrapper.vm.$nextTick() + + // Field-validated + const textField = wrapper.findComponent(FieldText) + await textField.find('input').setValue('Test emit') + await textField.find('input').trigger('blur') + expect(textField.emitted()).toHaveProperty('validated') + expect(wrapper.emitted()).toHaveProperty('field-validated') + + // Submit + const submitField = wrapper.findComponent(FieldSubmit) + await submitField.find('input').trigger('submit') + expect(submitField.emitted().submit).toHaveLength(1) + expect(wrapper.emitted().submit).toHaveLength(1) + + clearEmittedEvents(submitField) + clearEmittedEvents(wrapper) + + // No submit when form has errors + wrapper.vm.model.textModel = '' + wrapper.vm.formErrors = { textModel: 'Field is required' } + await submitField.find('input').trigger('submit') + expect(submitField.emitted().submit).toHaveLength(1) + expect(wrapper.emitted().submit).toHaveLength(0) + }) + }) \ No newline at end of file diff --git a/tests/components/FormGroup.spec.js b/tests/components/FormGroup.spec.js new file mode 100644 index 0000000..18d8ffb --- /dev/null +++ b/tests/components/FormGroup.spec.js @@ -0,0 +1,73 @@ +import { expect, it, describe, beforeAll } from 'vitest' +import { mount, config } from '@vue/test-utils' +import FieldText from '@/fields/core/FieldText.vue' +import FormGroup from '@/FormGroup.vue' +import { generateSchemaSingleField } from '@test/_resources/utils.js' + +const mountFormGroup = (props) => mount(FormGroup, { props }) + +beforeAll(() => { + config.global.components = { FieldText } +}) + +const textFieldSchema = generateSchemaSingleField( + 'testField', + 'testFieldModel', + 'input', + 'text', + 'Test label', + '' +) +const field = textFieldSchema.schema.fields[0] +const model = textFieldSchema.model + +describe('FormGroup', () => { + + it('Should render properly', async () => { + const wrapper = mountFormGroup({ field, model }) + const textField = wrapper.findComponent(FieldText) + expect(textField.exists()).toBeTruthy() + expect(wrapper.find('.field-wrap').exists()).toBeTruthy() + expect(wrapper.find('.form-group').exists()).toBeTruthy() + expect(wrapper.find('label').exists()).toBeTruthy() + }) + + it('Should hide field if not visible', async () => { + const localField = { ...field, visible: false } + const wrapper = mountFormGroup({ field: localField, model }) + const textField = wrapper.findComponent(FieldText) + expect(textField.exists()).toBeTruthy() + expect(wrapper.vm.fieldStyle).toHaveProperty('display', 'none') + }) + + it('Should not render label when field schema or component tells it not to render', async () => { + const localField = { ...field, noLabel: true } // noLabel: true here is the same as in the component + const wrapper = mountFormGroup({ field: localField, model }) + const textField = wrapper.findComponent(FieldText) + expect(textField.exists()).toBeTruthy() + expect(wrapper.find('label').exists()).toBeFalsy() + }) + + it('Should display errors', async () => { + const localField = { ...field, validator: () => false } + const wrapper = mountFormGroup({ field: localField, model }) + const textField = wrapper.findComponent(FieldText) + expect(textField.exists()).toBeTruthy() + await textField.find('input').trigger('blur') + expect(wrapper.vm.fieldHasErrors).toBeTruthy() + await wrapper.vm.$nextTick() + expect(wrapper.find('.errors').exists()).toBeTruthy() + expect(wrapper.find('.error').text()).toBe('Field is invalid') + }) + + it('Should display hints', async () => { + const localField = { ...field, hint: 'This is a hint' } + const wrapper = mountFormGroup({ field: localField, model }) + const textField = wrapper.findComponent(FieldText) + expect(textField.exists()).toBeTruthy() + await wrapper.vm.$nextTick() + expect(wrapper.find('.hint').exists()).toBeTruthy() + expect(wrapper.find('.hint').text()).toBe('This is a hint') + }) + +}) \ No newline at end of file