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 @@ + + + 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 @@ + + 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 @@