Merge pull request #610 from gradido/test-validation-rules

feat: Test Validation Rules
This commit is contained in:
Moriz Wahl 2021-07-07 17:11:30 +02:00 committed by GitHub
commit fd228a3942
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 262 additions and 185 deletions

View File

@ -206,7 +206,7 @@ jobs:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 37 min_coverage: 44
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -1,26 +1,11 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { extend } from 'vee-validate'
import InputPasswordConfirmation from './InputPasswordConfirmation' import InputPasswordConfirmation from './InputPasswordConfirmation'
const rules = [
'containsLowercaseCharacter',
'containsUppercaseCharacter',
'containsNumericCharacter',
'atLeastEightCharactera',
'samePassword',
]
rules.forEach((rule) => {
extend(rule, {
validate(value) {
return true
},
})
})
const localVue = global.localVue const localVue = global.localVue
// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js
describe('InputPasswordConfirmation', () => { describe('InputPasswordConfirmation', () => {
let wrapper let wrapper

View File

@ -2,7 +2,7 @@ import Vue from 'vue'
import DashboardPlugin from './plugins/dashboard-plugin' import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue' import App from './App.vue'
import i18n from './i18n.js' import i18n from './i18n.js'
import './validation-rules' import { loadAllRules } from './validation-rules'
import { store } from './store/store' import { store } from './store/store'
@ -12,6 +12,8 @@ import router from './routes/router'
Vue.use(DashboardPlugin) Vue.use(DashboardPlugin)
Vue.config.productionTip = false Vue.config.productionTip = false
loadAllRules(i18n)
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.sessionId) { if (to.meta.requiresAuth && !store.state.sessionId) {
next({ path: '/login' }) next({ path: '/login' })

View File

@ -1,5 +1,4 @@
import '@/polyfills' import '@/polyfills'
import { configure, extend } from 'vee-validate'
import GlobalComponents from './globalComponents' import GlobalComponents from './globalComponents'
import GlobalDirectives from './globalDirectives' import GlobalDirectives from './globalDirectives'
import SideBar from '@/components/SidebarPlugin' import SideBar from '@/components/SidebarPlugin'
@ -14,8 +13,6 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// asset imports // asset imports
import '@/assets/scss/argon.scss' import '@/assets/scss/argon.scss'
import '@/assets/vendor/nucleo/css/nucleo.css' import '@/assets/vendor/nucleo/css/nucleo.css'
import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
import VueQrcodeReader from 'vue-qrcode-reader' import VueQrcodeReader from 'vue-qrcode-reader'
import VueQrcode from 'vue-qrcode' import VueQrcode from 'vue-qrcode'
@ -28,13 +25,6 @@ import VueMoment from 'vue-moment'
import Loading from 'vue-loading-overlay' import Loading from 'vue-loading-overlay'
import 'vue-loading-overlay/dist/vue-loading.css' import 'vue-loading-overlay/dist/vue-loading.css'
Object.keys(rules).forEach((rule) => {
extend(rule, {
...rules[rule], // copies rule configuration
message: messages[rule], // assign message
})
})
export default { export default {
install(Vue) { install(Vue) {
Vue.use(GlobalComponents) Vue.use(GlobalComponents)
@ -49,12 +39,5 @@ export default {
Vue.use(VueQrcode) Vue.use(VueQrcode)
Vue.use(FlatPickr) Vue.use(FlatPickr)
Vue.use(Loading) Vue.use(Loading)
configure({
classes: {
valid: 'is-valid',
invalid: 'is-invalid',
dirty: ['is-dirty', 'is-dirty'], // multiple classes per flag!
},
})
}, },
} }

View File

@ -1,104 +1,110 @@
import i18n from './i18n.js'
import { configure, extend } from 'vee-validate' import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules' import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import loginAPI from './apis/loginAPI' import loginAPI from './apis/loginAPI'
configure({ export const loadAllRules = (i18nCallback) => {
configure({
defaultMessage: (field, values) => { defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`) values._field_ = i18nCallback.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values) return i18nCallback.t(`validations.messages.${values._rule_}`, values)
}, },
}) classes: {
valid: 'is-valid',
invalid: 'is-invalid',
dirty: ['is-dirty', 'is-dirty'], // multiple classes per flag!
},
})
extend('email', { extend('email', {
...email, ...email,
message: (_, values) => i18n.t('validations.messages.email', values), message: (_, values) => i18nCallback.t('validations.messages.email', values),
}) })
extend('required', { extend('required', {
...required, ...required,
message: (_, values) => i18n.t('validations.messages.required', values), message: (_, values) => i18nCallback.t('validations.messages.required', values),
}) })
extend('min', { extend('min', {
...min, ...min,
message: (_, values) => i18n.t('validations.messages.min', values), message: (_, values) => i18nCallback.t('validations.messages.min', values),
}) })
extend('max', { extend('max', {
...max, ...max,
message: (_, values) => i18n.t('validations.messages.max', values), message: (_, values) => i18nCallback.t('validations.messages.max', values),
}) })
extend('gddSendAmount', { extend('gddSendAmount', {
validate(value, { min, max }) { validate(value, { min, max }) {
value = value.replace(',', '.') value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
}, },
params: ['min', 'max'], params: ['min', 'max'],
message: (_, values) => { message: (_, values) => {
values.min = i18n.n(values.min, 'ungroupedDecimal') values.min = i18nCallback.n(values.min, 'ungroupedDecimal')
values.max = i18n.n(values.max, 'ungroupedDecimal') values.max = i18nCallback.n(values.max, 'ungroupedDecimal')
return i18n.t('form.validation.gddSendAmount', values) return i18nCallback.t('form.validation.gddSendAmount', values)
}, },
}) })
extend('gddUsernameUnique', { extend('gddUsernameUnique', {
async validate(value) { async validate(value) {
const result = await loginAPI.checkUsername(value) const result = await loginAPI.checkUsername(value)
return result.result.data.state === 'success' return result.result.data.state === 'success'
}, },
message: (_, values) => i18n.t('form.validation.usernmae-unique', values), message: (_, values) => i18nCallback.t('form.validation.usernmae-unique', values),
}) })
extend('gddUsernameRgex', { extend('gddUsernameRgex', {
validate(value) { validate(value) {
return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/) return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/)
}, },
message: (_, values) => i18n.t('form.validation.usernmae-regex', values), message: (_, values) => i18nCallback.t('form.validation.usernmae-regex', values),
}) })
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
extend('is_not', { extend('is_not', {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
...is_not, ...is_not,
message: (_, values) => i18n.t('form.validation.is-not', values), message: (_, values) => i18nCallback.t('form.validation.is-not', values),
}) })
// Password validation // Password validation
extend('containsLowercaseCharacter', { extend('containsLowercaseCharacter', {
validate(value) { validate(value) {
return !!value.match(/[a-z]+/) return !!value.match(/[a-z]+/)
}, },
message: (_, values) => i18n.t('site.signup.lowercase', values), message: (_, values) => i18nCallback.t('site.signup.lowercase', values),
}) })
extend('containsUppercaseCharacter', { extend('containsUppercaseCharacter', {
validate(value) { validate(value) {
return !!value.match(/[A-Z]+/) return !!value.match(/[A-Z]+/)
}, },
message: (_, values) => i18n.t('site.signup.uppercase', values), message: (_, values) => i18nCallback.t('site.signup.uppercase', values),
}) })
extend('containsNumericCharacter', { extend('containsNumericCharacter', {
validate(value) { validate(value) {
return !!value.match(/[0-9]+/) return !!value.match(/[0-9]+/)
}, },
message: (_, values) => i18n.t('site.signup.one_number', values), message: (_, values) => i18nCallback.t('site.signup.one_number', values),
}) })
extend('atLeastEightCharactera', { extend('atLeastEightCharactera', {
validate(value) { validate(value) {
return !!value.match(/.{8,}/) return !!value.match(/.{8,}/)
}, },
message: (_, values) => i18n.t('site.signup.minimum', values), message: (_, values) => i18nCallback.t('site.signup.minimum', values),
}) })
extend('samePassword', { extend('samePassword', {
validate(value, [pwd]) { validate(value, [pwd]) {
return value === pwd return value === pwd
}, },
message: (_, values) => i18n.t('site.signup.dont_match', values), message: (_, values) => i18nCallback.t('site.signup.dont_match', values),
}) })
}

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import TransactionForm from './TransactionForm' import TransactionForm from './TransactionForm'
import flushPromises from 'flush-promises'
const localVue = global.localVue const localVue = global.localVue
@ -19,8 +20,12 @@ describe('GddSend', () => {
}, },
} }
const propsData = {
balance: 100.0,
}
const Wrapper = () => { const Wrapper = () => {
return mount(TransactionForm, { localVue, mocks }) return mount(TransactionForm, { localVue, mocks, propsData })
} }
describe('mount', () => { describe('mount', () => {
@ -53,6 +58,18 @@ describe('GddSend', () => {
'E-Mail', 'E-Mail',
) )
}) })
it('flushes an error message when no valid email is given', async () => {
await wrapper.find('#input-group-1').find('input').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('validations.messages.email')
})
it('trims the email after blur', async () => {
await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ')
await flushPromises()
expect(wrapper.vm.form.email).toBe('valid@email.com')
})
}) })
describe('ammount field', () => { describe('ammount field', () => {
@ -73,6 +90,24 @@ describe('GddSend', () => {
'0.01', '0.01',
) )
}) })
it('flushes an error message when no valid amount is given', async () => {
await wrapper.find('#input-group-2').find('input').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
})
it('flushes an error message when amount is too high', async () => {
await wrapper.find('#input-group-2').find('input').setValue('123.34')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
})
it('flushes no errors when amount is valid', async () => {
await wrapper.find('#input-group-2').find('input').setValue('87.34')
await flushPromises()
expect(wrapper.find('span.errors').exists()).toBeFalsy()
})
}) })
describe('message text box', () => { describe('message text box', () => {
@ -89,6 +124,18 @@ describe('GddSend', () => {
it('has a label form.memo', () => { it('has a label form.memo', () => {
expect(wrapper.find('label.input-3').text()).toBe('form.memo') expect(wrapper.find('label.input-3').text()).toBe('form.memo')
}) })
it('flushes an error message when memo is less than 5 characters', async () => {
await wrapper.find('#input-group-3').find('textarea').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('validations.messages.min')
})
it('flushes no error message when memo is valid', async () => {
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
await flushPromises()
expect(wrapper.find('span.errors').exists()).toBeFalsy()
})
}) })
describe('cancel button', () => { describe('cancel button', () => {
@ -100,11 +147,42 @@ describe('GddSend', () => {
expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset') expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset')
}) })
it.skip('clears the email field on click', async () => { it('clears all fields on click', async () => {
wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
wrapper.find('button[type="reset"]').trigger('click') await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.vm.$nextTick() await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
expect(wrapper.vm.form.email).toBeNull() await flushPromises()
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
expect(wrapper.vm.form.amount).toBe('87.23')
expect(wrapper.vm.form.memo).toBe('Long enugh')
await wrapper.find('button[type="reset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.form.email).toBe('')
expect(wrapper.vm.form.amount).toBe('')
expect(wrapper.vm.form.memo).toBe('')
})
})
describe('submit', () => {
beforeEach(async () => {
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('emits set-transaction', async () => {
expect(wrapper.emitted('set-transaction')).toBeTruthy()
expect(wrapper.emitted('set-transaction')).toEqual([
[
{
email: 'someone@watches.tv',
amount: 87.23,
memo: 'Long enugh',
},
],
])
}) })
}) })
}) })

View File

@ -168,7 +168,7 @@ export default {
}, },
methods: { methods: {
onSubmit() { onSubmit() {
this.normalizeAmount() this.normalizeAmount(true)
this.$emit('set-transaction', { this.$emit('set-transaction', {
email: this.form.email, email: this.form.email,
amount: this.form.amountValue, amount: this.form.amountValue,
@ -181,10 +181,11 @@ export default {
this.form.amount = '' this.form.amount = ''
this.form.memo = '' this.form.memo = ''
}, },
/*
setTransaction(data) { setTransaction(data) {
this.form.email = data.email this.form.email = data.email
this.form.amount = data.amount this.form.amount = data.amount
}, }, */
normalizeAmount(isValid) { normalizeAmount(isValid) {
this.amountFocused = false this.amountFocused = false
if (!isValid) return if (!isValid) return

View File

@ -83,9 +83,7 @@ describe('ForgotPassword', () => {
}) })
it('displays an error', () => { it('displays an error', () => {
expect(form.find('div.invalid-feedback').text()).toEqual( expect(form.find('div.invalid-feedback').text()).toEqual('validations.messages.email')
'The Email field must be a valid email',
)
}) })
it('does not call the API', () => { it('does not call the API', () => {

View File

@ -123,7 +123,7 @@ describe('Login', () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
expect(wrapper.findAll('div.invalid-feedback').at(0).text()).toBe( expect(wrapper.findAll('div.invalid-feedback').at(0).text()).toBe(
'The Email field is required', 'validations.messages.required',
) )
}) })
@ -131,7 +131,7 @@ describe('Login', () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await flushPromises() await flushPromises()
expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe( expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe(
'The form.password field is required', 'validations.messages.required',
) )
}) })
}) })

View File

@ -84,7 +84,7 @@ describe('Register', () => {
wrapper.find('#registerEmail').setValue('no_valid@Email') wrapper.find('#registerEmail').setValue('no_valid@Email')
await flushPromises() await flushPromises()
await expect(wrapper.find('#registerEmailLiveFeedback').text()).toEqual( await expect(wrapper.find('#registerEmailLiveFeedback').text()).toEqual(
'The Email field must be a valid email', 'validations.messages.email',
) )
}) })

View File

@ -2,23 +2,8 @@ import { mount, RouterLinkStub } from '@vue/test-utils'
import loginAPI from '../../apis/loginAPI' import loginAPI from '../../apis/loginAPI'
import ResetPassword from './ResetPassword' import ResetPassword from './ResetPassword'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { extend } from 'vee-validate'
const rules = [ // validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js
'containsLowercaseCharacter',
'containsUppercaseCharacter',
'containsNumericCharacter',
'atLeastEightCharactera',
'samePassword',
]
rules.forEach((rule) => {
extend(rule, {
validate(value) {
return true
},
})
})
jest.mock('../../apis/loginAPI') jest.mock('../../apis/loginAPI')

View File

@ -2,23 +2,6 @@ import { mount } from '@vue/test-utils'
import UserCardFormPasswort from './UserCard_FormUserPasswort' import UserCardFormPasswort from './UserCard_FormUserPasswort'
import loginAPI from '../../../apis/loginAPI' import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { extend } from 'vee-validate'
const rules = [
'containsLowercaseCharacter',
'containsUppercaseCharacter',
'containsNumericCharacter',
'atLeastEightCharactera',
'samePassword',
]
rules.forEach((rule) => {
extend(rule, {
validate(value) {
return true
},
})
})
jest.mock('../../../apis/loginAPI') jest.mock('../../../apis/loginAPI')
@ -122,6 +105,57 @@ describe('UserCardFormUserPasswort', () => {
expect(form.find('button[type="submit"]').exists()).toBeTruthy() expect(form.find('button[type="submit"]').exists()).toBeTruthy()
}) })
describe('validation', () => {
it('displays all password requirements', () => {
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(5)
expect(feedbackArray.at(0).text()).toBe('validations.messages.required')
expect(feedbackArray.at(1).text()).toBe('site.signup.lowercase')
expect(feedbackArray.at(2).text()).toBe('site.signup.uppercase')
expect(feedbackArray.at(3).text()).toBe('site.signup.one_number')
expect(feedbackArray.at(4).text()).toBe('site.signup.minimum')
})
it('removes first message when a character is given', async () => {
await wrapper.findAll('input').at(1).setValue('@')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(4)
expect(feedbackArray.at(0).text()).toBe('site.signup.lowercase')
})
it('removes first and second message when a lowercase character is given', async () => {
await wrapper.findAll('input').at(1).setValue('a')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(3)
expect(feedbackArray.at(0).text()).toBe('site.signup.uppercase')
})
it('removes the first three messages when a lowercase and uppercase characters are given', async () => {
await wrapper.findAll('input').at(1).setValue('Aa')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(2)
expect(feedbackArray.at(0).text()).toBe('site.signup.one_number')
})
it('removes the first four messages when a lowercase, uppercase and numeric characters are given', async () => {
await wrapper.findAll('input').at(1).setValue('Aa1')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(1)
expect(feedbackArray.at(0).text()).toBe('site.signup.minimum')
})
it('removes all messages when all rules are fulfilled', async () => {
await wrapper.findAll('input').at(1).setValue('Aa123456')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(0)
})
})
describe('submit', () => { describe('submit', () => {
describe('valid data', () => { describe('valid data', () => {
beforeEach(async () => { beforeEach(async () => {

View File

@ -1,29 +1,24 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { extend } from 'vee-validate'
import UserCardFormUsername from './UserCard_FormUsername' import UserCardFormUsername from './UserCard_FormUsername'
import loginAPI from '../../../apis/loginAPI' import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import { extend } from 'vee-validate'
jest.mock('../../../apis/loginAPI') jest.mock('../../../apis/loginAPI')
extend('gddUsernameRgex', {
validate(value) {
return true
},
})
extend('gddUsernameUnique', {
validate(value) {
return true
},
})
const localVue = global.localVue const localVue = global.localVue
const mockAPIcall = jest.fn((args) => { const mockAPIcall = jest.fn((args) => {
return { success: true } return { success: true }
}) })
// override this rule to avoid API call
extend('gddUsernameUnique', {
validate(value) {
return true
},
})
const toastErrorMock = jest.fn() const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn() const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()

View File

@ -1,10 +1,11 @@
import { createLocalVue } from '@vue/test-utils' import { createLocalVue } from '@vue/test-utils'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate' import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'
import * as rules from 'vee-validate/dist/rules' import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json' import { messages } from 'vee-validate/dist/locale/en.json'
import RegeneratorRuntime from 'regenerator-runtime' import RegeneratorRuntime from 'regenerator-runtime'
import SideBar from '@/components/SidebarPlugin' import SideBar from '@/components/SidebarPlugin'
import VueQrcode from 'vue-qrcode' import VueQrcode from 'vue-qrcode'
@ -14,7 +15,7 @@ import VueMoment from 'vue-moment'
import clickOutside from '@/directives/click-ouside.js' import clickOutside from '@/directives/click-ouside.js'
import { focus } from 'vue-focus' import { focus } from 'vue-focus'
global.localVue = createLocalVue() import { loadAllRules } from '../src/validation-rules'
Object.keys(rules).forEach((rule) => { Object.keys(rules).forEach((rule) => {
extend(rule, { extend(rule, {
@ -23,6 +24,15 @@ Object.keys(rules).forEach((rule) => {
}) })
}) })
const i18nMock = {
t: (identifier, values) => identifier,
n: (value, format) => value,
}
loadAllRules(i18nMock)
global.localVue = createLocalVue()
global.localVue.use(BootstrapVue) global.localVue.use(BootstrapVue)
global.localVue.use(Vuex) global.localVue.use(Vuex)
global.localVue.use(IconsPlugin) global.localVue.use(IconsPlugin)