vee-validate
vee-validate copied to clipboard
Validation Errors Not Displayed in Snapshot Tests or When trying to get Error Messages with vee-validate in Nuxt 3
What happened?
When running snapshot tests for a login form using vee-validate with shadcn-vue in a Nuxt 3 application, the validation error messages are not displayed in the snapshot. The test simulates a button click to trigger form submission, but the expected error messages do not appear in the snapshot, indicating that the validations are not being triggered or resolved (I think this the real problem) correctly during the test.
Expected Behavior: The snapshot should contain the validation error messages after the button click triggers the form submission.
Actual Behavior: The snapshot does not contain the validation error messages, indicating that the validations are not being triggered or resolved correctly during the test.
Additional Context: The form validation works correctly when tested manually in the browser, but the issue only occurs during the automated tests.
Reproduction steps
- Set up a Nuxt 3 project with vee-validate for form validation.
- Create a login form component with vee-validate validation rules.
- Write a test using vitest and @vue/test-utils to simulate a button click and capture the snapshot.
- Run the test and observe that the snapshot does not contain the expected validation error messages.
Version
Vue.js 3.x and vee-validate 4.x
What browsers are you seeing the problem on?
- [ ] Firefox
- [ ] Chrome
- [ ] Safari
- [ ] Microsoft Edge
Relevant log output
// Test code
import { it, expect, describe, beforeEach } from 'vitest';
import { mountSuspended } from '@nuxt/test-utils/runtime';
import type { VueWrapper } from '@vue/test-utils';
import { flushPromises } from '@vue/test-utils';
import waitForExpect from 'wait-for-expect';
import App from '@/app.vue';
describe('Login page tests', () => {
let wrapper: VueWrapper;
beforeEach(async () => {
wrapper = await mountSuspended(App, {
route: '/login',
});
});
it('should match snapshot', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('should match snapshot after click button and inputs show errors', async () => {
const button = wrapper.findComponent('button[type="submit"]');
await button.trigger('click');
await flushPromises();
await waitForExpect(() => {
expect(wrapper.html()).toMatchSnapshot();
});
});
});
// Component code
<template>
<div class="w-full">
<div class="w-full text-center mb-2">
<h2 class="text-xl font-semibold">{{ $t('auth.login.title') }}</h2>
</div>
<form ref="form" novalidate @submit="onSubmit">
<FormField
v-slot="{ componentField }"
:validate-on-model-update="false"
name="email"
>
<FormItem class="mb-2">
<FormLabel>
{{ $t('auth.login.form.inputs.email.label') }}
</FormLabel>
<FormControl>
<Input
type="text"
:placeholder="$t('auth.login.form.inputs.email.placeholder')"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
:validate-on-model-update="false"
name="password"
>
<FormItem class="mb-4">
<FormLabel>
{{ $t('auth.login.form.inputs.password.label') }}
</FormLabel>
<FormControl>
<Input
type="password"
:placeholder="$t('auth.login.form.inputs.password.placeholder')"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit" class="w-full">
{{ $t('auth.login.form.button') }}
</Button>
</form>
<div class="my-4 flex flex-col w-full gap-4 justify-center items-center">
<NuxtLink :to="AuthRoutes.Recover" class="text-sm opacity-60">
{{ $t('auth.login.actions.recover') }}
</NuxtLink>
<div
class="w-full flex flex-col justify-center items-center border-t pt-2"
>
<p class="text-sm opacity-60">
{{ $t('auth.login.actions.register.description') }}
</p>
<NuxtLink :to="AuthRoutes.Register" class="text-sm font-bold">
{{ $t('auth.login.actions.register.action') }}
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup>
import { useForm } from 'vee-validate';
import { object, string } from 'yup';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { AuthRoutes } from '~/enums/routes';
definePageMeta({
layout: 'auth',
});
const { t } = useI18n();
const formSchema = object({
email: string()
.required(
t('validations.required', {
field: t('auth.login.form.inputs.email.label').toLowerCase(),
})
)
.email(
t('validations.valid', {
field: t('auth.login.form.inputs.email.label').toLowerCase(),
})
),
password: string().required(
t('validations.required', {
field: t('auth.login.form.inputs.password.label').toLowerCase(),
})
),
});
const form = useForm({
validationSchema: formSchema,
initialValues: {
email: '',
password: '',
},
});
const onSubmit = form.handleSubmit((values) => {
console.log('Form submitted!', values);
});
</script>
//; Test configuration
import { defineVitestConfig } from '@nuxt/test-utils/config';
export default defineVitestConfig({
test: {
setupFiles: ['tests/setup.ts'],
environmentOptions: {
nuxt: {
mock: {
intersectionObserver: true,
},
},
},
},
});
// setup.ts
import { config } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
import en from '@/lang/en';
const i18n = createI18n({
locale: 'en',
messages: { en },
missing: (_, key) => console.error(key),
});
config.global.plugins.push(i18n);
Demo link
Code of Conduct
- [x] I agree to follow this project's Code of Conduct