diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 002118f5b..4f35b87e1 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: 32
+ min_coverage: 33
token: ${{ github.token }}
##############################################################################
diff --git a/frontend/src/components/Inputs/InputEmail.spec.js b/frontend/src/components/Inputs/InputEmail.spec.js
new file mode 100644
index 000000000..f8e374654
--- /dev/null
+++ b/frontend/src/components/Inputs/InputEmail.spec.js
@@ -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')
+ })
+ })
+ })
+})
diff --git a/frontend/src/components/Inputs/InputEmail.vue b/frontend/src/components/Inputs/InputEmail.vue
new file mode 100644
index 000000000..41338fd8c
--- /dev/null
+++ b/frontend/src/components/Inputs/InputEmail.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ {{ errors[0] }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Inputs/InputPassword.spec.js b/frontend/src/components/Inputs/InputPassword.spec.js
new file mode 100644
index 000000000..ec446536d
--- /dev/null
+++ b/frontend/src/components/Inputs/InputPassword.spec.js
@@ -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)
+ })
+ })
+ })
+})
diff --git a/frontend/src/components/Inputs/InputPassword.vue b/frontend/src/components/Inputs/InputPassword.vue
new file mode 100644
index 000000000..e4fdd34b4
--- /dev/null
+++ b/frontend/src/components/Inputs/InputPassword.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ errors[0] }}
+
+
+
+
+
+
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json
index 4d85269c5..7d42be813 100644
--- a/frontend/src/locales/de.json
+++ b/frontend/src/locales/de.json
@@ -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":{
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 987605bd8..ce4199711 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -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":{
diff --git a/frontend/src/views/Pages/Login.spec.js b/frontend/src/views/Pages/Login.spec.js
index 0d94840b0..a4ff79bed 100644
--- a/frontend/src/views/Pages/Login.spec.js
+++ b/frontend/src/views/Pages/Login.spec.js
@@ -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')
+ })
+ })
+ })
+ })
})
})
diff --git a/frontend/src/views/Pages/Login.vue b/frontend/src/views/Pages/Login.vue
index 4619dddf2..2d4ce2345 100755
--- a/frontend/src/views/Pages/Login.vue
+++ b/frontend/src/views/Pages/Login.vue
@@ -13,7 +13,6 @@
-
@@ -22,76 +21,15 @@
{{ $t('login') }}
-
-
-
-
-
-
- {{ validationContext.errors[0] }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ validationContext.errors[0] }}
-
-
-
-
-
-
-
-
-
- Leider konnten wir keinen Account finden mit diesen Daten!
-
-
-
-
-
-
+
+
+
{{ $t('login') }}
@@ -118,32 +56,27 @@