mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #564 from gradido/password-component
refactor: Password Component
This commit is contained in:
commit
0059cffa19
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -206,7 +206,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 32
|
||||
min_coverage: 33
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
71
frontend/src/components/Inputs/InputEmail.spec.js
Normal file
71
frontend/src/components/Inputs/InputEmail.spec.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import InputEmail from './InputEmail'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('InputEmail', () => {
|
||||
let wrapper
|
||||
|
||||
const propsData = {
|
||||
name: 'input-field-name',
|
||||
label: 'input-field-label',
|
||||
placeholder: 'input-field-placeholder',
|
||||
value: '',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputEmail, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has an input field', () => {
|
||||
expect(wrapper.find('input').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('properties', () => {
|
||||
it('has the name "input-field-name"', () => {
|
||||
expect(wrapper.find('input').attributes('name')).toEqual('input-field-name')
|
||||
})
|
||||
|
||||
it('has the id "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('input').attributes('id')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
|
||||
})
|
||||
|
||||
it('has the value ""', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual('')
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toEqual('input-field-label')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('input').setValue('12')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['12']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('value property changes', () => {
|
||||
it('updates data model', async () => {
|
||||
await wrapper.setProps({ value: 'user@example.org' })
|
||||
expect(wrapper.vm.currentValue).toEqual('user@example.org')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
73
frontend/src/components/Inputs/InputEmail.vue
Normal file
73
frontend/src/components/Inputs/InputEmail.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<validation-provider
|
||||
tag="div"
|
||||
:rules="rules"
|
||||
:name="name"
|
||||
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor">
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
:id="labelFor"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="email"
|
||||
:state="validated ? valid : false"
|
||||
trim
|
||||
class="email-form-input"
|
||||
></b-form-input>
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputEmail',
|
||||
props: {
|
||||
rules: {
|
||||
default: () => {
|
||||
return {
|
||||
required: true,
|
||||
email: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
name: { type: String, default: 'Email' },
|
||||
label: { type: String, default: 'Email' },
|
||||
placeholder: { type: String, default: 'Email' },
|
||||
value: { required: true, type: String },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFor() {
|
||||
return this.name + '-input-field'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.email-form-input {
|
||||
border-right-style: solid !important;
|
||||
border-right-width: 1px !important;
|
||||
padding-right: 12px !important;
|
||||
border-top-right-radius: 6px !important;
|
||||
border-bottom-right-radius: 6px !important;
|
||||
}
|
||||
</style>
|
||||
98
frontend/src/components/Inputs/InputPassword.spec.js
Normal file
98
frontend/src/components/Inputs/InputPassword.spec.js
Normal file
@ -0,0 +1,98 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import InputPassword from './InputPassword'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('InputPassword', () => {
|
||||
let wrapper
|
||||
|
||||
const propsData = {
|
||||
name: 'input-field-name',
|
||||
label: 'input-field-label',
|
||||
placeholder: 'input-field-placeholder',
|
||||
value: '',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputPassword, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has an input field', () => {
|
||||
expect(wrapper.find('input').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('properties', () => {
|
||||
it('has the name "input-field-name"', () => {
|
||||
expect(wrapper.find('input').attributes('name')).toEqual('input-field-name')
|
||||
})
|
||||
|
||||
it('has the id "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('input').attributes('id')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
|
||||
})
|
||||
|
||||
it('has the value ""', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual('')
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toEqual('input-field-label')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('input').setValue('12')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['12']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('password visibilty', () => {
|
||||
it('has type password by default', () => {
|
||||
expect(wrapper.find('input').attributes('type')).toEqual('password')
|
||||
})
|
||||
|
||||
it('changes to type text when icon is clicked', async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('input').attributes('type')).toEqual('text')
|
||||
})
|
||||
|
||||
it('changes back to type password when icon is clicked twice', async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('input').attributes('type')).toEqual('password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('password visibilty icon', () => {
|
||||
it('is by default bi-eye-slash', () => {
|
||||
expect(wrapper.find('svg').classes('bi-eye-slash')).toBe(true)
|
||||
})
|
||||
|
||||
it('changes to bi-eye when clicked', async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('svg').classes('bi-eye')).toBe(true)
|
||||
})
|
||||
|
||||
it('changes back to bi-eye-slash when clicked twice', async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('svg').classes('bi-eye-slash')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
69
frontend/src/components/Inputs/InputPassword.vue
Normal file
69
frontend/src/components/Inputs/InputPassword.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<validation-provider
|
||||
tag="div"
|
||||
:rules="rules"
|
||||
:name="name"
|
||||
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor">
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
:id="labelFor"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:state="validated ? valid : false"
|
||||
></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-primary" @click="toggleShowPassword">
|
||||
<b-icon :icon="showPassword ? 'eye' : 'eye-slash'" />
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputPassword',
|
||||
props: {
|
||||
rules: {
|
||||
default: () => {
|
||||
return {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
name: { type: String, default: 'password' },
|
||||
label: { type: String, default: 'Password' },
|
||||
placeholder: { type: String, default: 'Password' },
|
||||
value: { required: true, type: String },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
showPassword: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFor() {
|
||||
return this.name + '-input-field'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowPassword() {
|
||||
this.showPassword = !this.showPassword
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -66,6 +66,7 @@
|
||||
},
|
||||
"error": {
|
||||
"error":"Fehler",
|
||||
"no-account": "Leider konnten wir keinen Account finden mit diesen Daten!",
|
||||
"change-password": "Fehler beim Ändern des Passworts"
|
||||
},
|
||||
"transaction":{
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
},
|
||||
"error": {
|
||||
"error":"Error",
|
||||
"no-account": "Unfortunately we could not find an account to the given data!",
|
||||
"change-password": "Error while changing password"
|
||||
},
|
||||
"transaction":{
|
||||
|
||||
@ -1,10 +1,37 @@
|
||||
import { mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
|
||||
import loginAPI from '../../apis/loginAPI'
|
||||
import Login from './Login'
|
||||
|
||||
jest.mock('../../apis/loginAPI')
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockLoginCall = jest.fn()
|
||||
mockLoginCall.mockReturnValue({
|
||||
success: true,
|
||||
result: {
|
||||
data: {
|
||||
session_id: 1,
|
||||
user: {
|
||||
name: 'Peter Lustig',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
loginAPI.login = mockLoginCall
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
const mockStoreDispach = jest.fn()
|
||||
const mockRouterPush = jest.fn()
|
||||
const spinnerHideMock = jest.fn()
|
||||
const spinnerMock = jest.fn(() => {
|
||||
return {
|
||||
hide: spinnerHideMock,
|
||||
}
|
||||
})
|
||||
|
||||
describe('Login', () => {
|
||||
let wrapper
|
||||
|
||||
@ -13,6 +40,18 @@ describe('Login', () => {
|
||||
locale: 'en',
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$store: {
|
||||
dispatch: mockStoreDispach,
|
||||
},
|
||||
$loading: {
|
||||
show: spinnerMock,
|
||||
},
|
||||
$router: {
|
||||
push: mockRouterPush,
|
||||
},
|
||||
$toast: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const stubs = {
|
||||
@ -76,16 +115,76 @@ describe('Login', () => {
|
||||
it('has a Submit button', () => {
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows a warning when no valid Email is entered', async () => {
|
||||
wrapper.find('input[placeholder="Email"]').setValue('no_valid@Email')
|
||||
await flushPromises()
|
||||
await expect(wrapper.find('.invalid-feedback').text()).toEqual(
|
||||
'The Email field must be a valid email',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// to do: test submit button
|
||||
describe('submit', () => {
|
||||
describe('no data', () => {
|
||||
it('displays a message that Email is required', async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('div.invalid-feedback').at(0).text()).toBe(
|
||||
'The Email field is required',
|
||||
)
|
||||
})
|
||||
|
||||
it('displays a message that password is required', async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe(
|
||||
'The password field is required',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid data', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('calls the API with the given data', () => {
|
||||
expect(mockLoginCall).toBeCalledWith('user@example.org', '1234')
|
||||
})
|
||||
|
||||
it('creates a spinner', () => {
|
||||
expect(spinnerMock).toBeCalled()
|
||||
})
|
||||
|
||||
describe('login success', () => {
|
||||
it('dispatches server response to store', () => {
|
||||
expect(mockStoreDispach).toBeCalledWith('login', {
|
||||
sessionId: 1,
|
||||
user: { name: 'Peter Lustig' },
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to overview page', () => {
|
||||
expect(mockRouterPush).toBeCalledWith('/overview')
|
||||
})
|
||||
|
||||
it('hides the spinner', () => {
|
||||
expect(spinnerHideMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('login fails', () => {
|
||||
beforeEach(() => {
|
||||
mockLoginCall.mockReturnValue({ success: false })
|
||||
})
|
||||
|
||||
it('hides the spinner', () => {
|
||||
expect(spinnerHideMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('error.no-account')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
<!-- Page content -->
|
||||
<b-container class="mt--8">
|
||||
<b-row class="justify-content-center">
|
||||
<b-col lg="5" md="7">
|
||||
@ -22,76 +21,15 @@
|
||||
<div class="text-center text-muted mb-4">
|
||||
<small>{{ $t('login') }}</small>
|
||||
</div>
|
||||
|
||||
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
||||
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
|
||||
<validation-provider
|
||||
name="Email"
|
||||
:rules="{ required: true, email: true }"
|
||||
v-slot="validationContext"
|
||||
>
|
||||
<b-form-group class="mb-3" label="Email" label-for="login-email">
|
||||
<b-form-input
|
||||
id="login-email"
|
||||
name="example-input-1"
|
||||
v-model="form.email"
|
||||
placeholder="Email"
|
||||
:state="getValidationState(validationContext)"
|
||||
aria-describedby="login-email-live-feedback"
|
||||
></b-form-input>
|
||||
|
||||
<b-form-invalid-feedback id="login-email-live-feedback">
|
||||
{{ validationContext.errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
|
||||
<validation-provider
|
||||
:name="$t('form.password')"
|
||||
:rules="{ required: true }"
|
||||
v-slot="validationContext"
|
||||
>
|
||||
<b-form-group
|
||||
class="mb-5"
|
||||
id="example-input-group-1"
|
||||
:label="$t('form.password')"
|
||||
label-for="example-input-1"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
id="input-pwd"
|
||||
name="input-pwd"
|
||||
v-model="form.password"
|
||||
:placeholder="$t('form.password')"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
:state="getValidationState(validationContext)"
|
||||
aria-describedby="input-2-live-feedback"
|
||||
></b-form-input>
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-primary" @click="togglePasswordVisibility">
|
||||
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<b-form-invalid-feedback id="input-2-live-feedback">
|
||||
{{ validationContext.errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
|
||||
<b-alert v-show="loginfail" show dismissible variant="warning">
|
||||
<span class="alert-text bv-example-row">
|
||||
<b-row>
|
||||
<b-col class="col-9 text-left text-dark">
|
||||
<strong>
|
||||
Leider konnten wir keinen Account finden mit diesen Daten!
|
||||
</strong>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</span>
|
||||
</b-alert>
|
||||
<div class="text-center">
|
||||
<input-email v-model="form.email"></input-email>
|
||||
<input-password
|
||||
:label="$t('form.password')"
|
||||
:placeholder="$t('form.password')"
|
||||
v-model="form.password"
|
||||
></input-password>
|
||||
<div class="text-center mt-4">
|
||||
<b-button type="submit" variant="primary">{{ $t('login') }}</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
@ -118,32 +56,27 @@
|
||||
<script>
|
||||
import loginAPI from '../../apis/loginAPI'
|
||||
import CONFIG from '../../config'
|
||||
import InputPassword from '../../components/Inputs/InputPassword'
|
||||
import InputEmail from '../../components/Inputs/InputEmail'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
components: {
|
||||
InputPassword,
|
||||
InputEmail,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
email: '',
|
||||
password: '',
|
||||
// rememberMe: false
|
||||
},
|
||||
loginfail: false,
|
||||
allowRegister: CONFIG.ALLOW_REGISTER,
|
||||
passwordVisible: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null
|
||||
},
|
||||
|
||||
togglePasswordVisibility() {
|
||||
this.passwordVisible = !this.passwordVisible
|
||||
},
|
||||
async onSubmit() {
|
||||
// error info ausschalten
|
||||
this.loginfail = false
|
||||
const loader = this.$loading.show({
|
||||
container: this.$refs.submitButton,
|
||||
})
|
||||
@ -157,7 +90,7 @@ export default {
|
||||
loader.hide()
|
||||
} else {
|
||||
loader.hide()
|
||||
this.loginfail = true
|
||||
this.$toast.error(this.$t('error.no-account'))
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user