diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4aec1dfd3..a7f7b73c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -206,7 +206,7 @@ jobs: report_name: Coverage Frontend type: lcov result_path: ./coverage/lcov.info - min_coverage: 37 + min_coverage: 44 token: ${{ github.token }} ############################################################################## diff --git a/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js b/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js index ef3b5d64e..953d0b960 100644 --- a/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js +++ b/frontend/src/components/Inputs/InputPasswordConfirmation.spec.js @@ -1,26 +1,11 @@ import { mount } from '@vue/test-utils' -import { extend } from 'vee-validate' import InputPasswordConfirmation from './InputPasswordConfirmation' -const rules = [ - 'containsLowercaseCharacter', - 'containsUppercaseCharacter', - 'containsNumericCharacter', - 'atLeastEightCharactera', - 'samePassword', -] - -rules.forEach((rule) => { - extend(rule, { - validate(value) { - return true - }, - }) -}) - const localVue = global.localVue +// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js + describe('InputPasswordConfirmation', () => { let wrapper diff --git a/frontend/src/main.js b/frontend/src/main.js index 0e6fd0ef2..9e1b4c06b 100755 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -2,7 +2,7 @@ import Vue from 'vue' import DashboardPlugin from './plugins/dashboard-plugin' import App from './App.vue' import i18n from './i18n.js' -import './validation-rules' +import { loadAllRules } from './validation-rules' import { store } from './store/store' @@ -12,6 +12,8 @@ import router from './routes/router' Vue.use(DashboardPlugin) Vue.config.productionTip = false +loadAllRules(i18n) + router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !store.state.sessionId) { next({ path: '/login' }) diff --git a/frontend/src/plugins/dashboard-plugin.js b/frontend/src/plugins/dashboard-plugin.js index 2edac0995..8ef34e4ab 100755 --- a/frontend/src/plugins/dashboard-plugin.js +++ b/frontend/src/plugins/dashboard-plugin.js @@ -1,5 +1,4 @@ import '@/polyfills' -import { configure, extend } from 'vee-validate' import GlobalComponents from './globalComponents' import GlobalDirectives from './globalDirectives' import SideBar from '@/components/SidebarPlugin' @@ -14,8 +13,6 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' // asset imports import '@/assets/scss/argon.scss' 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 VueQrcode from 'vue-qrcode' @@ -28,13 +25,6 @@ import VueMoment from 'vue-moment' import Loading from 'vue-loading-overlay' 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 { install(Vue) { Vue.use(GlobalComponents) @@ -49,12 +39,5 @@ export default { Vue.use(VueQrcode) Vue.use(FlatPickr) Vue.use(Loading) - configure({ - classes: { - valid: 'is-valid', - invalid: 'is-invalid', - dirty: ['is-dirty', 'is-dirty'], // multiple classes per flag! - }, - }) }, } diff --git a/frontend/src/validation-rules.js b/frontend/src/validation-rules.js index e92e820bb..59900b272 100644 --- a/frontend/src/validation-rules.js +++ b/frontend/src/validation-rules.js @@ -1,104 +1,110 @@ -import i18n from './i18n.js' import { configure, extend } from 'vee-validate' // eslint-disable-next-line camelcase import { required, email, min, max, is_not } from 'vee-validate/dist/rules' import loginAPI from './apis/loginAPI' -configure({ - defaultMessage: (field, values) => { - values._field_ = i18n.t(`fields.${field}`) - return i18n.t(`validations.messages.${values._rule_}`, values) - }, -}) +export const loadAllRules = (i18nCallback) => { + configure({ + defaultMessage: (field, values) => { + values._field_ = i18nCallback.t(`fields.${field}`) + 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', { - ...email, - message: (_, values) => i18n.t('validations.messages.email', values), -}) + extend('email', { + ...email, + message: (_, values) => i18nCallback.t('validations.messages.email', values), + }) -extend('required', { - ...required, - message: (_, values) => i18n.t('validations.messages.required', values), -}) + extend('required', { + ...required, + message: (_, values) => i18nCallback.t('validations.messages.required', values), + }) -extend('min', { - ...min, - message: (_, values) => i18n.t('validations.messages.min', values), -}) + extend('min', { + ...min, + message: (_, values) => i18nCallback.t('validations.messages.min', values), + }) -extend('max', { - ...max, - message: (_, values) => i18n.t('validations.messages.max', values), -}) + extend('max', { + ...max, + message: (_, values) => i18nCallback.t('validations.messages.max', values), + }) -extend('gddSendAmount', { - validate(value, { min, max }) { - value = value.replace(',', '.') - return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max - }, - params: ['min', 'max'], - message: (_, values) => { - values.min = i18n.n(values.min, 'ungroupedDecimal') - values.max = i18n.n(values.max, 'ungroupedDecimal') - return i18n.t('form.validation.gddSendAmount', values) - }, -}) + extend('gddSendAmount', { + validate(value, { min, max }) { + value = value.replace(',', '.') + return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max + }, + params: ['min', 'max'], + message: (_, values) => { + values.min = i18nCallback.n(values.min, 'ungroupedDecimal') + values.max = i18nCallback.n(values.max, 'ungroupedDecimal') + return i18nCallback.t('form.validation.gddSendAmount', values) + }, + }) -extend('gddUsernameUnique', { - async validate(value) { - const result = await loginAPI.checkUsername(value) - return result.result.data.state === 'success' - }, - message: (_, values) => i18n.t('form.validation.usernmae-unique', values), -}) + extend('gddUsernameUnique', { + async validate(value) { + const result = await loginAPI.checkUsername(value) + return result.result.data.state === 'success' + }, + message: (_, values) => i18nCallback.t('form.validation.usernmae-unique', values), + }) -extend('gddUsernameRgex', { - validate(value) { - return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/) - }, - message: (_, values) => i18n.t('form.validation.usernmae-regex', values), -}) + extend('gddUsernameRgex', { + validate(value) { + return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/) + }, + message: (_, values) => i18nCallback.t('form.validation.usernmae-regex', values), + }) -// eslint-disable-next-line camelcase -extend('is_not', { // eslint-disable-next-line camelcase - ...is_not, - message: (_, values) => i18n.t('form.validation.is-not', values), -}) + extend('is_not', { + // eslint-disable-next-line camelcase + ...is_not, + message: (_, values) => i18nCallback.t('form.validation.is-not', values), + }) -// Password validation + // Password validation -extend('containsLowercaseCharacter', { - validate(value) { - return !!value.match(/[a-z]+/) - }, - message: (_, values) => i18n.t('site.signup.lowercase', values), -}) + extend('containsLowercaseCharacter', { + validate(value) { + return !!value.match(/[a-z]+/) + }, + message: (_, values) => i18nCallback.t('site.signup.lowercase', values), + }) -extend('containsUppercaseCharacter', { - validate(value) { - return !!value.match(/[A-Z]+/) - }, - message: (_, values) => i18n.t('site.signup.uppercase', values), -}) + extend('containsUppercaseCharacter', { + validate(value) { + return !!value.match(/[A-Z]+/) + }, + message: (_, values) => i18nCallback.t('site.signup.uppercase', values), + }) -extend('containsNumericCharacter', { - validate(value) { - return !!value.match(/[0-9]+/) - }, - message: (_, values) => i18n.t('site.signup.one_number', values), -}) + extend('containsNumericCharacter', { + validate(value) { + return !!value.match(/[0-9]+/) + }, + message: (_, values) => i18nCallback.t('site.signup.one_number', values), + }) -extend('atLeastEightCharactera', { - validate(value) { - return !!value.match(/.{8,}/) - }, - message: (_, values) => i18n.t('site.signup.minimum', values), -}) + extend('atLeastEightCharactera', { + validate(value) { + return !!value.match(/.{8,}/) + }, + message: (_, values) => i18nCallback.t('site.signup.minimum', values), + }) -extend('samePassword', { - validate(value, [pwd]) { - return value === pwd - }, - message: (_, values) => i18n.t('site.signup.dont_match', values), -}) + extend('samePassword', { + validate(value, [pwd]) { + return value === pwd + }, + message: (_, values) => i18nCallback.t('site.signup.dont_match', values), + }) +} diff --git a/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.spec.js b/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.spec.js index 199cba4f3..28b769a3c 100644 --- a/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.spec.js +++ b/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils' import TransactionForm from './TransactionForm' +import flushPromises from 'flush-promises' const localVue = global.localVue @@ -19,8 +20,12 @@ describe('GddSend', () => { }, } + const propsData = { + balance: 100.0, + } + const Wrapper = () => { - return mount(TransactionForm, { localVue, mocks }) + return mount(TransactionForm, { localVue, mocks, propsData }) } describe('mount', () => { @@ -53,6 +58,18 @@ describe('GddSend', () => { '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', () => { @@ -73,6 +90,24 @@ describe('GddSend', () => { '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', () => { @@ -89,6 +124,18 @@ describe('GddSend', () => { it('has a label 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', () => { @@ -100,11 +147,42 @@ describe('GddSend', () => { expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset') }) - it.skip('clears the email field on click', async () => { - wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') - wrapper.find('button[type="reset"]').trigger('click') - await wrapper.vm.$nextTick() - expect(wrapper.vm.form.email).toBeNull() + it('clears all fields on click', 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 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', + }, + ], + ]) }) }) }) diff --git a/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.vue b/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.vue index c9c9df7b3..74a4a8de1 100644 --- a/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.vue +++ b/frontend/src/views/Pages/AccountOverview/GddSend/TransactionForm.vue @@ -168,7 +168,7 @@ export default { }, methods: { onSubmit() { - this.normalizeAmount() + this.normalizeAmount(true) this.$emit('set-transaction', { email: this.form.email, amount: this.form.amountValue, @@ -181,10 +181,11 @@ export default { this.form.amount = '' this.form.memo = '' }, - setTransaction(data) { - this.form.email = data.email - this.form.amount = data.amount - }, + /* + setTransaction(data) { + this.form.email = data.email + this.form.amount = data.amount + }, */ normalizeAmount(isValid) { this.amountFocused = false if (!isValid) return diff --git a/frontend/src/views/Pages/ForgotPassword.spec.js b/frontend/src/views/Pages/ForgotPassword.spec.js index 4e6c3b834..df60568b9 100644 --- a/frontend/src/views/Pages/ForgotPassword.spec.js +++ b/frontend/src/views/Pages/ForgotPassword.spec.js @@ -83,9 +83,7 @@ describe('ForgotPassword', () => { }) it('displays an error', () => { - expect(form.find('div.invalid-feedback').text()).toEqual( - 'The Email field must be a valid email', - ) + expect(form.find('div.invalid-feedback').text()).toEqual('validations.messages.email') }) it('does not call the API', () => { diff --git a/frontend/src/views/Pages/Login.spec.js b/frontend/src/views/Pages/Login.spec.js index 2339cd341..4be82684a 100644 --- a/frontend/src/views/Pages/Login.spec.js +++ b/frontend/src/views/Pages/Login.spec.js @@ -123,7 +123,7 @@ describe('Login', () => { await wrapper.find('form').trigger('submit') await flushPromises() 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 flushPromises() expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe( - 'The form.password field is required', + 'validations.messages.required', ) }) }) diff --git a/frontend/src/views/Pages/Register.spec.js b/frontend/src/views/Pages/Register.spec.js index 36ee6987b..85f3ca38f 100644 --- a/frontend/src/views/Pages/Register.spec.js +++ b/frontend/src/views/Pages/Register.spec.js @@ -84,7 +84,7 @@ describe('Register', () => { wrapper.find('#registerEmail').setValue('no_valid@Email') await flushPromises() await expect(wrapper.find('#registerEmailLiveFeedback').text()).toEqual( - 'The Email field must be a valid email', + 'validations.messages.email', ) }) diff --git a/frontend/src/views/Pages/ResetPassword.spec.js b/frontend/src/views/Pages/ResetPassword.spec.js index 11bdad1ba..a0a86a24d 100644 --- a/frontend/src/views/Pages/ResetPassword.spec.js +++ b/frontend/src/views/Pages/ResetPassword.spec.js @@ -2,23 +2,8 @@ import { mount, RouterLinkStub } from '@vue/test-utils' import loginAPI from '../../apis/loginAPI' import ResetPassword from './ResetPassword' 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 - }, - }) -}) +// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js jest.mock('../../apis/loginAPI') diff --git a/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js b/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js index fb435cedb..f00a4d2b4 100644 --- a/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js +++ b/frontend/src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js @@ -2,23 +2,6 @@ import { mount } from '@vue/test-utils' import UserCardFormPasswort from './UserCard_FormUserPasswort' import loginAPI from '../../../apis/loginAPI' 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') @@ -122,6 +105,57 @@ describe('UserCardFormUserPasswort', () => { 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('valid data', () => { beforeEach(async () => { diff --git a/frontend/src/views/Pages/UserProfile/UserCard_FormUsername.spec.js b/frontend/src/views/Pages/UserProfile/UserCard_FormUsername.spec.js index 48bbe6b70..8b1c53751 100644 --- a/frontend/src/views/Pages/UserProfile/UserCard_FormUsername.spec.js +++ b/frontend/src/views/Pages/UserProfile/UserCard_FormUsername.spec.js @@ -1,29 +1,24 @@ import { mount } from '@vue/test-utils' -import { extend } from 'vee-validate' import UserCardFormUsername from './UserCard_FormUsername' import loginAPI from '../../../apis/loginAPI' import flushPromises from 'flush-promises' +import { extend } from 'vee-validate' jest.mock('../../../apis/loginAPI') -extend('gddUsernameRgex', { - validate(value) { - return true - }, -}) - -extend('gddUsernameUnique', { - validate(value) { - return true - }, -}) - const localVue = global.localVue const mockAPIcall = jest.fn((args) => { return { success: true } }) +// override this rule to avoid API call +extend('gddUsernameUnique', { + validate(value) { + return true + }, +}) + const toastErrorMock = jest.fn() const toastSuccessMock = jest.fn() const storeCommitMock = jest.fn() diff --git a/frontend/test/testSetup.js b/frontend/test/testSetup.js index 26d311941..565ebc33f 100644 --- a/frontend/test/testSetup.js +++ b/frontend/test/testSetup.js @@ -1,10 +1,11 @@ import { createLocalVue } from '@vue/test-utils' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import Vuex from 'vuex' + import { ValidationProvider, ValidationObserver, extend } from 'vee-validate' import * as rules from 'vee-validate/dist/rules' - import { messages } from 'vee-validate/dist/locale/en.json' + import RegeneratorRuntime from 'regenerator-runtime' import SideBar from '@/components/SidebarPlugin' import VueQrcode from 'vue-qrcode' @@ -14,7 +15,7 @@ import VueMoment from 'vue-moment' import clickOutside from '@/directives/click-ouside.js' import { focus } from 'vue-focus' -global.localVue = createLocalVue() +import { loadAllRules } from '../src/validation-rules' Object.keys(rules).forEach((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(Vuex) global.localVue.use(IconsPlugin)