From e8277861ec5172e62ae42ea044522918e4b9e62f Mon Sep 17 00:00:00 2001 From: MateuszMichalowski <79852198+MateuszMichalowski@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:53:40 +0200 Subject: [PATCH] fix(frontend): vue3 migration pre deploy setup (#3366) * fix(admin): update test files predeploy * fix(admin): update test files predeploy * fix(admin): update test files predeploy --- admin/.eslintrc.js | 4 +- admin/jest.config.js | 39 - admin/package.json | 27 +- admin/src/App.spec.js | 71 +- .../components/ChangeUserRoleFormular.spec.js | 848 +-- .../src/components/ChangeUserRoleFormular.vue | 2 +- .../ConfirmRegisterMailFormular.spec.js | 103 +- admin/src/components/ContentFooter.spec.js | 28 +- .../ContributionLink/ContributionLink.spec.js | 203 +- .../ContributionLinkForm.spec.js | 261 +- .../ContributionLinkList.spec.js | 223 +- .../ContributionMessagesFormular.spec.js | 372 +- .../ContributionMessagesList.spec.js | 289 +- .../ContributionMessagesListItem.spec.js | 339 +- admin/src/components/CreationFormular.spec.js | 479 +- admin/src/components/CreationFormular.vue | 31 +- .../CreationTransactionList.spec.js | 209 +- .../components/DeletedUserFormular.spec.js | 362 +- .../components/EditCreationFormular.spec.js | 295 +- admin/src/components/EditCreationFormular.vue | 16 +- .../Federation/CommunityVisualizeItem.spec.js | 949 ++-- .../Federation/CommunityVisualizeItem.vue | 8 +- admin/src/components/FigureQrCode.spec.js | 82 +- admin/src/components/NavBar.spec.js | 194 +- admin/src/components/NavBar.vue | 6 +- admin/src/components/NotFoundPage.spec.js | 43 +- admin/src/components/Overlay.spec.js | 82 +- .../Tables/OpenCreationsTable.spec.js | 267 +- .../components/Tables/OpenCreationsTable.vue | 32 +- .../components/Tables/SearchUserTable.spec.js | 188 +- .../src/components/Tables/SearchUserTable.vue | 11 +- .../components/Tables/StatisticTable.spec.js | 136 +- .../src/components/Tables/StatisticTable.vue | 11 +- .../components/TransactionLinkList.spec.js | 272 +- admin/src/components/UserQuery.spec.js | 116 +- .../src/components/input/Coordinates.spec.js | 62 +- admin/src/components/input/Coordinates.vue | 26 +- .../components/input/EditableGroup.spec.js | 87 +- .../input/EditableGroupableLabel.spec.js | 107 +- admin/src/components/input/TimePicker.spec.js | 35 +- admin/src/i18n.test.js | 89 +- admin/src/locales/index.test.js | 64 +- admin/src/main.js | 34 +- admin/src/main.test.js | 138 +- admin/src/mixins/creationMonths.js | 62 - admin/src/mixins/toaster.js | 30 - admin/src/mixins/toggleRowDetails.js | 34 - admin/src/mixins/toggleRowDetails.test.js | 141 - admin/src/pages/CommunityStatistic.spec.js | 163 +- admin/src/pages/ContributionLinks.spec.js | 130 +- admin/src/pages/CreationConfirm.spec.js | 704 +-- admin/src/pages/FederationVisualize.spec.js | 219 +- admin/src/pages/Overview.spec.js | 250 +- admin/src/pages/Overview.vue | 32 +- admin/src/pages/UserSearch.spec.js | 382 +- admin/src/plugins/apolloProvider.test.js | 182 +- admin/src/router/guards.test.js | 275 +- admin/src/router/router.test.js | 155 +- admin/src/store/store.test.js | 185 +- admin/test/testSetup.js | 23 - admin/test/vitest.setup.js | 42 + admin/vitest.config.js | 43 + admin/yarn.lock | 4566 +++-------------- frontend/.eslintrc.js | 4 +- frontend/babel.config.js | 2 +- frontend/jest.config.js | 32 - frontend/package.json | 27 +- frontend/src/App.spec.js | 169 +- frontend/src/components/ContentFooter.spec.js | 81 +- .../ContributionMessagesFormular.spec.js | 193 +- .../ContributionMessagesList.spec.js | 91 +- .../ContributionMessagesListItem.spec.js | 383 +- .../Contributions/ContributionForm.spec.js | 597 +-- .../Contributions/ContributionList.spec.js | 67 +- .../ContributionListItem.spec.js | 61 +- .../Contributions/OpenCreationsAmount.spec.js | 92 +- .../CollapseLinksList.spec.js | 239 +- frontend/src/components/GddSend.spec.js | 17 +- .../TransactionConfirmationLink.spec.js | 172 +- .../TransactionConfirmationSend.spec.js | 163 +- .../GddSend/TransactionForm.spec.js | 680 +-- .../components/GddSend/TransactionForm.vue | 2 +- .../src/components/GddTransactionList.spec.js | 306 +- .../GddTransactionListFooter.spec.js | 36 +- .../src/components/GdtTransactionList.spec.js | 114 +- .../src/components/Inputs/FirstName.spec.js | 49 +- .../src/components/Inputs/InputAmount.spec.js | 183 +- .../src/components/Inputs/InputEmail.spec.js | 83 +- .../src/components/Inputs/InputHour.spec.js | 158 +- .../components/Inputs/InputPassword.spec.js | 79 +- .../Inputs/InputPasswordConfirmation.spec.js | 64 +- .../components/Inputs/InputTextarea.spec.js | 166 +- .../components/Inputs/InputUsername.spec.js | 126 +- .../src/components/Inputs/LastName.spec.js | 79 +- .../src/components/LanguageSwitch.spec.js | 248 - .../src/components/LanguageSwitch.spec.unused | 250 + ...nguageSwitch.vue => LanguageSwitch.unused} | 0 .../src/components/LanguageSwitch2.spec.js | 226 +- frontend/src/components/Menu/Navbar.spec.js | 132 +- frontend/src/components/Menu/Sidebar.spec.js | 139 +- .../src/components/Message/Message.spec.js | 52 +- .../components/QrCode/FigureQrCode.spec.js | 123 +- .../components/SessionLogoutTimeout.spec.js | 189 +- .../src/components/SessionLogoutTimeout.vue | 10 +- frontend/src/components/Status.spec.js | 24 +- .../ContentHeader/CommunityMember.spec.js | 68 +- .../Template/ContentHeader/GddAmount.spec.js | 240 +- .../Template/ContentHeader/GdtAmount.spec.js | 230 +- .../RightSide/LastTransactions.spec.js | 100 +- frontend/src/components/Transaction.spec.js | 401 +- .../components/TransactionCollapse.spec.js | 72 +- .../TransactionLinks/TransactionLink.spec.js | 379 +- .../TransactionRows/AmountAndNameRow.spec.js | 129 +- .../TransactionRows/DateRow.spec.js | 71 +- .../components/TransactionRows/Name.spec.js | 51 +- .../Transactions/TransactionCreation.spec.js | 181 +- .../Transactions/TransactionDecay.spec.js | 115 +- .../TransactionLinkSummary.spec.js | 417 +- .../Transactions/TransactionReceive.spec.js | 101 +- .../Transactions/TransactionSend.spec.js | 92 +- .../components/UserSettings/UserCard.spec.js | 99 +- .../components/UserSettings/UserData.spec.js | 161 - .../UserSettings/UserData.spec.unused | 162 + .../{UserData.vue => UserData.unused} | 0 .../UserGMSLocationFormat.spec.js | 103 +- .../UserSettings/UserLanguage.spec.js | 163 - .../UserSettings/UserLanguage.spec.unused | 165 + .../{UserLanguage.vue => UserLanguage.unused} | 0 .../components/UserSettings/UserName.spec.js | 278 +- .../src/components/UserSettings/UserName.vue | 2 +- .../UserSettings/UserNamingFormat.spec.js | 113 +- .../UserSettings/UserNewsletter.spec.js | 201 +- .../UserSettings/UserPassword.spec.js | 411 +- .../components/UserSettings/UserPassword.vue | 2 - frontend/src/config/index.spec.js | 3 +- frontend/src/filters/amount.js | 2 +- frontend/src/filters/amount.test.js | 33 +- frontend/src/layouts/AuthLayout.spec.js | 174 +- frontend/src/layouts/DashboardLayout.spec.js | 531 +- frontend/src/layouts/DashboardLayout.vue | 5 +- .../templates/CommunityTemplate.spec.js | 90 +- .../src/layouts/templates/RightSide.spec.js | 37 +- frontend/src/locales/de.json | 2 - frontend/src/locales/en.json | 2 - frontend/src/main.js | 9 +- frontend/src/pages/Circles.spec.js | 145 +- frontend/src/pages/Community.spec.js | 717 +-- frontend/src/pages/ForgotPassword.spec.js | 345 +- frontend/src/pages/InfoStatistic.spec.js | 252 +- frontend/src/pages/Login.spec.js | 433 +- frontend/src/pages/NotFoundPage.spec.js | 60 +- frontend/src/pages/Overview.spec.js | 62 +- frontend/src/pages/Register.spec.js | 427 +- frontend/src/pages/RegisterCommunity.spec.js | 142 +- frontend/src/pages/ResetPassword.spec.js | 682 ++- frontend/src/pages/ResetPassword.vue | 121 - frontend/src/pages/Send.spec.js | 607 +-- frontend/src/pages/Send.vue | 6 +- frontend/src/pages/Settings.spec.js | 174 +- frontend/src/pages/TransactionLink.spec.js | 492 +- frontend/src/pages/TransactionLink.vue | 2 +- frontend/src/pages/Transactions.spec.js | 297 +- frontend/src/pages/UserSearch.spec.js | 149 +- frontend/src/pages/UserSearch.vue | 12 +- frontend/src/plugins/apolloProvider.test.js | 301 +- frontend/src/plugins/dashboard-plugin.test.js | 27 - .../src/plugins/dashboard-plugin.test.unused | 27 + ...oard-plugin.js => dashboard-plugin.unused} | 0 frontend/src/plugins/globalComponents.js | 10 - frontend/src/plugins/globalComponents.test.js | 29 - frontend/src/plugins/globalDirectives.test.js | 21 - .../src/plugins/globalDirectives.test.unused | 21 + frontend/src/routes/guards.test.js | 142 +- frontend/src/routes/router.test.js | 331 +- frontend/src/store/store.test.js | 386 +- frontend/test/testSetup.js | 73 - frontend/test/vitest.setup.js | 89 + frontend/vitest.config.js | 40 + frontend/yarn.lock | 3902 +++++--------- 179 files changed, 15630 insertions(+), 21055 deletions(-) delete mode 100644 admin/jest.config.js delete mode 100644 admin/src/mixins/creationMonths.js delete mode 100644 admin/src/mixins/toaster.js delete mode 100644 admin/src/mixins/toggleRowDetails.js delete mode 100644 admin/src/mixins/toggleRowDetails.test.js delete mode 100644 admin/test/testSetup.js create mode 100644 admin/test/vitest.setup.js create mode 100644 admin/vitest.config.js delete mode 100644 frontend/jest.config.js delete mode 100644 frontend/src/components/LanguageSwitch.spec.js create mode 100644 frontend/src/components/LanguageSwitch.spec.unused rename frontend/src/components/{LanguageSwitch.vue => LanguageSwitch.unused} (100%) delete mode 100644 frontend/src/components/UserSettings/UserData.spec.js create mode 100644 frontend/src/components/UserSettings/UserData.spec.unused rename frontend/src/components/UserSettings/{UserData.vue => UserData.unused} (100%) delete mode 100644 frontend/src/components/UserSettings/UserLanguage.spec.js create mode 100644 frontend/src/components/UserSettings/UserLanguage.spec.unused rename frontend/src/components/UserSettings/{UserLanguage.vue => UserLanguage.unused} (100%) delete mode 100644 frontend/src/plugins/dashboard-plugin.test.js create mode 100644 frontend/src/plugins/dashboard-plugin.test.unused rename frontend/src/plugins/{dashboard-plugin.js => dashboard-plugin.unused} (100%) delete mode 100755 frontend/src/plugins/globalComponents.js delete mode 100644 frontend/src/plugins/globalComponents.test.js delete mode 100644 frontend/src/plugins/globalDirectives.test.js create mode 100644 frontend/src/plugins/globalDirectives.test.unused delete mode 100644 frontend/test/testSetup.js create mode 100644 frontend/test/vitest.setup.js create mode 100644 frontend/vitest.config.js diff --git a/admin/.eslintrc.js b/admin/.eslintrc.js index b3477d848..e1b419178 100644 --- a/admin/.eslintrc.js +++ b/admin/.eslintrc.js @@ -3,7 +3,6 @@ module.exports = { env: { browser: true, node: true, - jest: true, 'vue/setup-compiler-macros': true, }, parserOptions: { @@ -17,7 +16,7 @@ module.exports = { 'prettier', ], // required to lint *.vue files - plugins: ['vue', 'prettier', 'jest'], + plugins: ['vue', 'prettier'], overrides: [ { files: ['*.json'], @@ -28,6 +27,7 @@ module.exports = { rules: { 'no-console': ['error'], 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'node/no-callback-literal': 0, // This is here to allow tests run properly 'vue/component-name-in-template-casing': ['error', 'kebab-case'], // 'vue/no-static-inline-styles': [ // 'error', diff --git a/admin/jest.config.js b/admin/jest.config.js deleted file mode 100644 index a0c8ae412..000000000 --- a/admin/jest.config.js +++ /dev/null @@ -1,39 +0,0 @@ -module.exports = { - verbose: true, - collectCoverage: true, - collectCoverageFrom: [ - 'src/**/*.{js,vue}', - '!**/node_modules/**', - '!src/assets/**', - '!**/?(*.)+(spec|test).js?(x)', - ], - coverageThreshold: { - global: { - lines: 95, - }, - }, - moduleFileExtensions: [ - 'js', - // 'jsx', - 'json', - 'vue', - ], - // coverageReporters: ['lcov', 'text'], - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - '\\.(css|less)$': 'identity-obj-proxy', - }, - transform: { - '^.+\\.vue$': 'vue-jest', - '^.+\\.(js|jsx)?$': 'babel-jest', - '/node_modules/vee-validate/dist/rules': 'babel-jest', - }, - setupFiles: ['/test/testSetup.js', 'jest-canvas-mock'], - testMatch: ['**/?(*.)+(spec|test).js?(x)'], - // snapshotSerializers: ['jest-serializer-vue'], - transformIgnorePatterns: [ - '/node_modules/(?!vee-validate/dist/rules)', - '/node_modules/(?!@babel)', - ], - testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen -} diff --git a/admin/package.json b/admin/package.json index 413bc7154..3a5d9363b 100644 --- a/admin/package.json +++ b/admin/package.json @@ -14,8 +14,10 @@ "postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", - "test": "echo Tests are temporarly disabled for migration time", - "test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit --no-cache --watch --runInBand", + "test": "cross-env TZ=UTC vitest run", + "test:coverage": "cross-env TZ=UTC vitest run --coverage", + "test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs", + "test:watch": "cross-env TZ=UTC vitest", "locales": "scripts/sort.sh" }, "dependencies": { @@ -27,13 +29,10 @@ "@vitejs/plugin-vue": "3.2.0", "@vue/apollo-composable": "^4.0.2", "@vue/apollo-option": "^4.0.0", - "@vue/cli-plugin-unit-jest": "~5.0.8", "@vue/compat": "3.4.31", "@vue/eslint-config-prettier": "^6.0.0", - "@vue/test-utils": "^1.2.2", "apollo-boost": "^0.4.9", "babel-core": "7.0.0-bridge.0", - "babel-jest": "^27.3.1", "babel-plugin-component": "^1.1.1", "babel-preset-env": "^1.7.0", "babel-preset-vue": "^2.0.2", @@ -45,9 +44,6 @@ "graphql": "^16.9.0", "graphql-tag": "^2.12.6", "identity-obj-proxy": "^3.0.0", - "jest": "26.6.3", - "jest-canvas-mock": "^2.3.1", - "jest-environment-jsdom-sixteen": "^2.0.0", "portal-vue": "3.0.0", "qrcanvas-vue": "3.0.0", "regenerator-runtime": "^0.13.9", @@ -57,7 +53,6 @@ "vue": "3.4.31", "vue-apollo": "3.1.2", "vue-i18n": "9.13.1", - "vue-jest": "3.0.7", "vue-router": "4.4.0", "vuex": "4.1.0", "vuex-persistedstate": "4.1.0" @@ -65,7 +60,9 @@ "devDependencies": { "@apollo/client": "^3.10.8", "@intlify/eslint-plugin-vue-i18n": "^1.4.0", + "@vitest/coverage-v8": "^2.0.5", "@vue/compiler-sfc": "^3.4.32", + "@vue/test-utils": "^2.4.6", "babel-plugin-transform-require-context": "^0.1.1", "cross-env": "^7.0.3", "eslint": "8.57.0", @@ -73,12 +70,11 @@ "eslint-config-standard": "^16.0.3", "eslint-loader": "^4.0.2", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "5.2.1", "eslint-plugin-promise": "^5.1.1", "eslint-plugin-vue": "8.7.1", - "jest": "29.7.0", + "jsdom": "^25.0.0", "mock-apollo-client": "^1.2.1", "postcss": "^8.4.8", "postcss-html": "^1.3.0", @@ -89,7 +85,9 @@ "stylelint-config-standard-scss": "13.1.0", "unplugin-icons": "^0.19.0", "unplugin-vue-components": "^0.27.3", - "vite-plugin-environment": "^1.1.3" + "vite-plugin-environment": "^1.1.3", + "vitest": "^2.0.5", + "vitest-canvas-mock": "^0.3.3" }, "browserslist": [ "> 1%", @@ -100,5 +98,10 @@ "ignore": [ "**/*.spec.js" ] + }, + "resolutions": { + "strip-ansi": "6.0.1", + "string-width": "4.2.2", + "wrap-ansi": "7.0.0" } } diff --git a/admin/src/App.spec.js b/admin/src/App.spec.js index 6936394f1..89baeabb6 100644 --- a/admin/src/App.spec.js +++ b/admin/src/App.spec.js @@ -1,34 +1,63 @@ +import { describe, it, expect, beforeEach } from 'vitest' import { shallowMount } from '@vue/test-utils' -import App from './App' +import { createStore } from 'vuex' +import { createRouter, createWebHistory } from 'vue-router' +import App from './App.vue' +import defaultLayout from '@/layouts/defaultLayout' -const localVue = global.localVue +const router = createRouter({ + history: createWebHistory(), + routes: [{ path: '/', component: { template: '
Mock Route
' } }], +}) -const stubs = { - RouterView: true, -} - -const mocks = { - $store: { - state: { - token: null, +const createVuexStore = (initialState = { token: null }) => { + return createStore({ + state() { + return initialState }, - }, + }) } -describe('App', () => { +describe('App.vue', () => { + let store let wrapper - const Wrapper = () => { - return shallowMount(App, { localVue, stubs, mocks }) + const createWrapper = (token = null) => { + store = createVuexStore({ token }) + return shallowMount(App, { + global: { + plugins: [store, router], + stubs: { + BToastOrchestrator: true, + BModalOrchestrator: true, + defaultLayout: true, + }, + }, + }) } - describe('shallowMount', () => { - beforeEach(() => { - wrapper = Wrapper() - }) + beforeEach(() => { + wrapper = createWrapper() + }) - it('has a div with id "app"', () => { - expect(wrapper.find('div#app').exists()).toBeTruthy() - }) + it('div#app is present', () => { + expect(wrapper.find('div#app').exists()).toBe(true) + }) + + it('renders default layout when token is present', () => { + wrapper = createWrapper('some-token') + + expect(wrapper.findComponent(defaultLayout).exists()).toBe(true) + expect(wrapper.find('router-view-stub').exists()).toBe(false) + }) + + it('does not render defaultLayout when token is not present', () => { + expect(wrapper.findComponent(defaultLayout).exists()).toBe(false) + expect(wrapper.find('router-view-stub').exists()).toBe(true) + }) + + it('always renders BToastOrchestrator and BModalOrchestrator', () => { + expect(wrapper.findComponent({ name: 'BToastOrchestrator' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'BModalOrchestrator' }).exists()).toBe(true) }) }) diff --git a/admin/src/components/ChangeUserRoleFormular.spec.js b/admin/src/components/ChangeUserRoleFormular.spec.js index 5af22d257..792812fe6 100644 --- a/admin/src/components/ChangeUserRoleFormular.spec.js +++ b/admin/src/components/ChangeUserRoleFormular.spec.js @@ -1,22 +1,23 @@ import { mount } from '@vue/test-utils' -import ChangeUserRoleFormular from './ChangeUserRoleFormular' -import { setUserRole } from '../graphql/setUserRole' -import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue' +import { useMutation } from '@vue/apollo-composable' +import { useStore } from 'vuex' -const localVue = global.localVue +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key) => key, + }), +})) -const apolloMutateMock = jest.fn().mockResolvedValue({ - data: { - setUserRole: null, - }, -}) +vi.mock('@vue/apollo-composable', () => ({ + useMutation: vi.fn(() => ({ + mutate: vi.fn(), + })), +})) -const mocks = { - $t: jest.fn((t) => t), - $apollo: { - mutate: apolloMutateMock, - }, - $store: { +vi.mock('vuex', () => ({ + useStore: vi.fn(() => ({ state: { moderator: { id: 0, @@ -24,648 +25,225 @@ const mocks = { roles: ['ADMIN'], }, }, - }, + })), +})) + +vi.mock('@/composables/useToast', () => ({ + useAppToast: () => ({ + toastSuccess: vi.fn(), + toastError: vi.fn(), + }), +})) + +const mockBFormSelect = { + name: 'BFormSelect', + template: '', + props: ['modelValue', 'options'], +} +const mockBButton = { + name: 'BButton', + template: '', } -let propsData -let wrapper -let spy - describe('ChangeUserRoleFormular', () => { - const Wrapper = () => { - return mount(ChangeUserRoleFormular, { localVue, mocks, propsData }) + let wrapper + let propsData + + beforeEach(() => { + vi.clearAllMocks() + }) + + const createWrapper = () => { + return mount(ChangeUserRoleFormular, { + props: propsData, + global: { + stubs: { + BFormSelect: mockBFormSelect, + BButton: mockBButton, + }, + mocks: { + $t: (key) => key, + }, + }, + }) } - describe('mount', () => { + describe('DOM elements', () => { beforeEach(() => { - jest.clearAllMocks() + propsData = { + item: { + userId: 1, + roles: [], + }, + } + wrapper = createWrapper() }) - describe('DOM has', () => { + it('has a DIV element with the class change-user-role-formular', () => { + expect(wrapper.find('.change-user-role-formular').exists()).toBe(true) + }) + }) + + describe('change own role', () => { + beforeEach(() => { + propsData = { + item: { + userId: 0, + roles: ['ADMIN'], + }, + } + wrapper = createWrapper() + }) + + it('has the text that you cannot change own role', () => { + expect(wrapper.text()).toContain('userRole.notChangeYourSelf') + }) + + it('has no role select', () => { + expect(wrapper.find('[data-testid="mock-bformselect"]').exists()).toBe(false) + }) + + it('has no button', () => { + expect(wrapper.find('[data-testid="mock-bbutton"]').exists()).toBe(false) + }) + }) + + describe("change other user's role", () => { + beforeEach(() => { + propsData = { + item: { + userId: 1, + roles: [], + }, + } + wrapper = createWrapper() + }) + + it('has no text that you cannot change own role', () => { + expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf') + }) + + it('has the select label', () => { + expect(wrapper.text()).toContain('userRole.selectLabel') + }) + + it('has a select', () => { + expect(wrapper.find('[data-testid="mock-bformselect"]').exists()).toBe(true) + }) + + it('has "change_user_role" button', () => { + const button = wrapper.find('[data-testid="mock-bbutton"]') + expect(button.exists()).toBe(true) + expect(button.text()).toBe('change_user_role') + }) + + describe('user has role "usual user"', () => { beforeEach(() => { - propsData = { - item: { - userId: 1, - roles: [], - }, - } - wrapper = Wrapper() + propsData.item.roles = ['USER'] + wrapper = createWrapper() }) - it('has a DIV element with the class.delete-user-formular', () => { - expect(wrapper.find('.change-user-role-formular').exists()).toBe(true) - }) - }) - - describe('change own role', () => { - beforeEach(() => { - propsData = { - item: { - userId: 0, - roles: ['ADMIN'], - }, - } - wrapper = Wrapper() + it('has selected option set to "USER"', () => { + expect(wrapper.vm.roleSelected).toBe('USER') }) - it('has the text that you cannot change own role', () => { - expect(wrapper.text()).toContain('userRole.notChangeYourSelf') - }) - - it('has no role select', () => { - expect(wrapper.find('select.role-select').exists()).toBe(false) - }) - - it('has no button', () => { - expect(wrapper.find('button.btn.btn-dange').exists()).toBe(false) - }) - }) - - describe("change other user's role", () => { - let rolesToSelect - - describe('general', () => { - beforeEach(() => { - propsData = { - item: { - userId: 1, - roles: [], - }, - } - wrapper = Wrapper() - rolesToSelect = wrapper.find('select.role-select').findAll('option') + describe('change select to new role "MODERATOR"', () => { + beforeEach(async () => { + wrapper.vm.roleSelected = 'MODERATOR' + await wrapper.vm.$nextTick() }) - it('has no text that you cannot change own role', () => { - expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf') + it('has "change_user_role" button enabled', () => { + const button = wrapper.find('[data-testid="mock-bbutton"]') + expect(button.attributes('disabled')).toBeFalsy() }) - it('has the select label', () => { - expect(wrapper.text()).toContain('userRole.selectLabel') - }) - - it('has a select', () => { - expect(wrapper.find('select.role-select').exists()).toBe(true) - }) - - it('has role select enabled', () => { - expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false) - }) - - it('has "change_user_role" button', () => { - expect(wrapper.find('button.btn.btn-danger').text()).toBe('change_user_role') - }) - }) - - describe('user has role "usual user"', () => { - beforeEach(() => { - apolloMutateMock.mockResolvedValue({ - data: { - setUserRole: 'ADMIN', - }, - }) - propsData = { - item: { - userId: 1, - roles: ['USER'], - }, - } - wrapper = Wrapper() - rolesToSelect = wrapper.find('select.role-select').findAll('option') - }) - - it('has selected option set to "usual user"', () => { - expect(wrapper.find('select.role-select').element.value).toBe('USER') - }) - - describe('change select to', () => { - describe('same role', () => { - it('has "change_user_role" button disabled', () => { - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true) - }) - - it('does not call the API', () => { - rolesToSelect.at(0).setSelected() - expect(apolloMutateMock).not.toHaveBeenCalled() - }) + describe('clicking the "change_user_role" button', () => { + beforeEach(async () => { + await wrapper.find('[data-testid="mock-bbutton"]').trigger('click') }) - describe('new role "MODERATOR"', () => { - beforeEach(() => { - rolesToSelect.at(1).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'MODERATOR', - }, - }), - ) - }) - - it('emits "updateRoles" with role moderator', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: ['MODERATOR'], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) - }) - - describe('new role "ADMIN"', () => { - beforeEach(() => { - rolesToSelect.at(2).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'ADMIN', - }, - }), - ) - }) - - it('emits "updateRoles" with role moderator', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: ['ADMIN'], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) + it('emits "show-modal" event', () => { + expect(wrapper.emitted('show-modal')).toBeTruthy() }) }) }) - - describe('user has role "moderator"', () => { - beforeEach(() => { - apolloMutateMock.mockResolvedValue({ - data: { - setUserRole: null, - }, - }) - propsData = { - item: { - userId: 1, - roles: ['MODERATOR'], - }, - } - wrapper = Wrapper() - rolesToSelect = wrapper.find('select.role-select').findAll('option') - }) - - it('has selected option set to "MODERATOR"', () => { - expect(wrapper.find('select.role-select').element.value).toBe('MODERATOR') - }) - - describe('change select to', () => { - describe('same role', () => { - it('has "change_user_role" button disabled', () => { - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true) - }) - - it('does not call the API', () => { - rolesToSelect.at(1).setSelected() - expect(apolloMutateMock).not.toHaveBeenCalled() - }) - }) - - describe('new role "USER"', () => { - beforeEach(() => { - rolesToSelect.at(0).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'USER', - }, - }), - ) - }) - - it('emits "updateRoles"', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: [], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) - }) - - describe('new role "ADMIN"', () => { - beforeEach(() => { - rolesToSelect.at(2).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'ADMIN', - }, - }), - ) - }) - - it('emits "updateRoles"', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: ['ADMIN'], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) - }) - }) - }) - - describe('user has role "admin"', () => { - beforeEach(() => { - apolloMutateMock.mockResolvedValue({ - data: { - setUserRole: null, - }, - }) - propsData = { - item: { - userId: 1, - roles: ['ADMIN'], - }, - } - wrapper = Wrapper() - rolesToSelect = wrapper.find('select.role-select').findAll('option') - }) - - it('has selected option set to "admin"', () => { - expect(wrapper.find('select.role-select').element.value).toBe('ADMIN') - }) - - describe('change select to', () => { - describe('same role', () => { - it('has "change_user_role" button disabled', () => { - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true) - }) - - it('does not call the API', () => { - rolesToSelect.at(1).setSelected() - // TODO: Fix this - expect(apolloMutateMock).not.toHaveBeenCalled() - }) - }) - - describe('new role "USER"', () => { - beforeEach(() => { - rolesToSelect.at(0).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'USER', - }, - }), - ) - }) - - it('emits "updateRoles"', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: [], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) - }) - - describe('new role "MODERATOR"', () => { - beforeEach(() => { - rolesToSelect.at(1).setSelected() - }) - - it('has "change_user_role" button enabled', () => { - expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true) - expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe( - false, - ) - }) - - describe('clicking the "change_user_role" button', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('calls the modal', () => { - expect(wrapper.emitted('showModal')) - expect(spy).toHaveBeenCalled() - }) - - describe('confirm role change with success', () => { - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: setUserRole, - variables: { - userId: 1, - role: 'MODERATOR', - }, - }), - ) - }) - - it('emits "updateRoles"', () => { - expect(wrapper.emitted('updateRoles')).toEqual( - expect.arrayContaining([ - expect.arrayContaining([ - { - userId: 1, - roles: ['MODERATOR'], - }, - ]), - ]), - ) - }) - - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo') - }) - }) - - describe('confirm role change with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh no!' }) - await wrapper.find('button').trigger('click') - await wrapper.vm.$nextTick() - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh no!') - }) - }) - }) - }) - }) - }) - }) - - describe('authenticated user is MODERATOR', () => { - beforeEach(() => { - mocks.$store.state.moderator.roles = ['MODERATOR'] - }) - - it('displays text with role', () => { - expect(wrapper.text()).toBe('userRole.selectRoles.admin') - }) - - it('has no role select', () => { - expect(wrapper.find('select.role-select').exists()).toBe(false) - }) - - it('has no button', () => { - expect(wrapper.find('button.btn.btn-dange').exists()).toBe(false) - }) + }) + }) + + describe('authenticated user is MODERATOR', () => { + beforeEach(() => { + vi.mocked(useStore).mockReturnValue({ + state: { + moderator: { + id: 0, + name: 'test moderator', + roles: ['MODERATOR'], + }, + }, + }) + propsData = { + item: { + userId: 1, + roles: [], + }, + } + wrapper = createWrapper() + }) + + it('has no role select', () => { + expect(wrapper.find('[data-testid="mock-bformselect"]').exists()).toBe(false) + }) + + it('has no button', () => { + expect(wrapper.find('[data-testid="mock-bbutton"]').exists()).toBe(false) + }) + }) + + describe('updateUserRole method', () => { + let mockMutate + + beforeEach(() => { + mockMutate = vi.fn() + useMutation.mockReturnValue({ + mutate: mockMutate, + }) + + propsData = { + item: { + userId: 1, + roles: ['USER'], + }, + } + wrapper = createWrapper() + }) + + it('calls setUserRole mutation and emits update-roles on success', async () => { + mockMutate.mockResolvedValue({ data: { setUserRole: 'MODERATOR' } }) + + await wrapper.vm.updateUserRole('MODERATOR', 'USER') + + expect(mockMutate).toHaveBeenCalledWith({ + userId: 1, + role: 'MODERATOR', + }) + expect(wrapper.emitted('update-roles')).toBeTruthy() + expect(wrapper.emitted('update-roles')[0]).toEqual([ + { + userId: 1, + roles: ['MODERATOR'], + }, + ]) + }) + + it('handles error and resets role on failure', async () => { + mockMutate.mockRejectedValue(new Error('API Error')) + + await wrapper.vm.updateUserRole('MODERATOR', 'USER') + + expect(mockMutate).toHaveBeenCalled() + expect(wrapper.vm.roleSelected).toBe('USER') + expect(wrapper.emitted('update-roles')).toBeFalsy() }) }) }) diff --git a/admin/src/components/ChangeUserRoleFormular.vue b/admin/src/components/ChangeUserRoleFormular.vue index 618dabbfc..a43a9bf32 100644 --- a/admin/src/components/ChangeUserRoleFormular.vue +++ b/admin/src/components/ChangeUserRoleFormular.vue @@ -2,7 +2,7 @@
- {{ roles.find((role) => role.value === currentRole.value).text }} + {{ roles.find((role) => role.value === currentRole.value)?.text }}
{{ $t('userRole.notChangeYourSelf') }} diff --git a/admin/src/components/ConfirmRegisterMailFormular.spec.js b/admin/src/components/ConfirmRegisterMailFormular.spec.js index 45cca10f7..59f7f7ebf 100644 --- a/admin/src/components/ConfirmRegisterMailFormular.spec.js +++ b/admin/src/components/ConfirmRegisterMailFormular.spec.js @@ -1,69 +1,72 @@ import { mount } from '@vue/test-utils' -import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue' +import { useMutation } from '@vue/apollo-composable' +import { useI18n } from 'vue-i18n' +import { useAppToast } from '@/composables/useToast' -import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' - -const localVue = global.localVue - -const apolloMutateMock = jest.fn().mockResolvedValue() - -const mocks = { - $t: jest.fn((t) => t), - $apollo: { - mutate: apolloMutateMock, - }, -} - -const propsData = { - checked: false, - email: 'bob@baumeister.de', - dateLastSend: '', -} +vi.mock('@vue/apollo-composable') +vi.mock('vue-i18n') +vi.mock('@/composables/useToast') describe('ConfirmRegisterMailFormular', () => { let wrapper + const mockMutate = vi.fn() + const mockT = vi.fn((key) => key) + const mockToastSuccess = vi.fn() + const mockToastError = vi.fn() - const Wrapper = () => { - return mount(ConfirmRegisterMailFormular, { localVue, mocks, propsData }) - } - - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() + beforeEach(() => { + useMutation.mockReturnValue({ + mutate: mockMutate, }) - it('has a DIV element with the class.component-confirm-register-mail', () => { - expect(wrapper.find('.component-confirm-register-mail').exists()).toBeTruthy() + useI18n.mockReturnValue({ + t: mockT, }) - describe('send register mail with success', () => { - beforeEach(() => { - wrapper.find('button.test-button').trigger('click') - }) + useAppToast.mockReturnValue({ + toastSuccess: mockToastSuccess, + toastError: mockToastError, + }) - it('calls the API with email', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - variables: { email: 'bob@baumeister.de' }, - }), - ) - }) + wrapper = mount(ConfirmRegisterMailFormular, { + props: { + checked: false, + email: 'bob@baumeister.de', + dateLastSend: '', + }, + global: { + mocks: { + $t: mockT, + }, + }, + }) + }) - it('toasts a success message', () => { - expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success') + it('renders the component', () => { + expect(wrapper.find('.component-confirm-register-mail').exists()).toBe(true) + }) + + describe('send register mail', () => { + it('calls the API with email on button click', async () => { + mockMutate.mockResolvedValueOnce({}) + await wrapper.find('button.test-button').trigger('click') + expect(mockMutate).toHaveBeenCalledWith({ + email: 'bob@baumeister.de', }) }) - describe('send register mail with error', () => { - beforeEach(() => { - apolloMutateMock.mockRejectedValue({ message: 'OUCH!' }) - wrapper = Wrapper() - wrapper.find('button.test-button').trigger('click') - }) + it('shows success message on successful API call', async () => { + mockMutate.mockResolvedValueOnce({}) + await wrapper.find('button.test-button').trigger('click') + expect(mockToastSuccess).toHaveBeenCalledWith('unregister_mail.success') + }) - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('unregister_mail.error') - }) + it('shows error message on failed API call', async () => { + mockMutate.mockRejectedValueOnce(new Error('OUCH!')) + await wrapper.find('button.test-button').trigger('click') + expect(mockToastError).toHaveBeenCalledWith('unregister_mail.error') }) }) }) diff --git a/admin/src/components/ContentFooter.spec.js b/admin/src/components/ContentFooter.spec.js index b7b8d5aef..f3ccb3562 100644 --- a/admin/src/components/ContentFooter.spec.js +++ b/admin/src/components/ContentFooter.spec.js @@ -1,29 +1,15 @@ import { mount } from '@vue/test-utils' -import ContentFooter from './ContentFooter' - -const localVue = global.localVue - -const mocks = { - $t: jest.fn((t) => t), - $i18n: { - locale: jest.fn(() => 'en'), - }, -} +import { describe, it, expect, beforeEach } from 'vitest' +import ContentFooter from './ContentFooter.vue' describe('ContentFooter', () => { let wrapper - const Wrapper = () => { - return mount(ContentFooter, { localVue, mocks }) - } + beforeEach(() => { + wrapper = mount(ContentFooter, {}) + }) - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() - }) - - it('renders the div element ".content-footer"', () => { - expect(wrapper.find('div.content-footer').exists()).toBe(true) - }) + it('renders the footer', () => { + expect(wrapper.find('.content-footer').exists()).toBe(true) }) }) diff --git a/admin/src/components/ContributionLink/ContributionLink.spec.js b/admin/src/components/ContributionLink/ContributionLink.spec.js index 85117e071..4c00166aa 100644 --- a/admin/src/components/ContributionLink/ContributionLink.spec.js +++ b/admin/src/components/ContributionLink/ContributionLink.spec.js @@ -1,114 +1,125 @@ import { mount } from '@vue/test-utils' -import ContributionLink from './ContributionLink' +import { describe, it, expect, beforeEach } from 'vitest' +import ContributionLink from './ContributionLink.vue' +import { BButton, BCard, BCardText, BCollapse } from 'bootstrap-vue-next' -const localVue = global.localVue - -const mocks = { - $t: jest.fn((t) => t), - $d: jest.fn((d) => d), -} - -const propsData = { - items: [ - { - id: 1, - name: 'Meditation', - memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', - amount: '200', - validFrom: '2022-04-01', - validTo: '2022-08-01', - cycle: 'täglich', - maxPerCycle: '3', - maxAmountPerMonth: 0, - link: 'https://localhost/redeem/CL-1a2345678', - }, - ], - count: 1, -} +const mockItems = [ + { + id: 1, + name: 'Meditation', + memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', + amount: '200', + validFrom: '2022-04-01', + validTo: '2022-08-01', + cycle: 'täglich', + maxPerCycle: '3', + maxAmountPerMonth: 0, + link: 'https://localhost/redeem/CL-1a2345678', + }, +] describe('ContributionLink', () => { let wrapper - const Wrapper = () => { - return mount(ContributionLink, { localVue, mocks, propsData }) + const createWrapper = () => { + return mount(ContributionLink, { + props: { + items: mockItems, + count: 1, + }, + global: { + mocks: { + $t: (key) => key, + $d: (d) => d, + }, + stubs: { + BCard, + BButton, + BCollapse, + BCardText, + ContributionLinkForm: true, + ContributionLinkList: true, + }, + }, + }) } - describe('mount', () => { + beforeEach(() => { + wrapper = createWrapper() + }) + + it('renders the Div Element ".contribution-link"', () => { + expect(wrapper.find('div.contribution-link').exists()).toBe(true) + }) + + it('has ContributionLinkList component when count > 0', () => { + expect(wrapper.findComponent({ name: 'ContributionLinkList' }).exists()).toBe(true) + }) + + it('shows "no contribution links" message when count is 0', async () => { + await wrapper.setProps({ count: 0 }) + expect(wrapper.text()).toContain('contributionLink.noContributionLinks') + }) + + it('has contribution form not visible by default', () => { + expect(wrapper.vm.visible).toBe(false) + }) + + describe('click on create new contribution', () => { + beforeEach(async () => { + await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click') + }) + + it('shows the contribution form', () => { + expect(wrapper.vm.visible).toBe(true) + }) + + it('hides the form when clicked again', async () => { + await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click') + expect(wrapper.vm.visible).toBe(false) + }) + }) + + describe('edit contribution link', () => { + beforeEach(async () => { + wrapper.vm.editContributionLinkData(mockItems[0]) + }) + + it('shows the contribution form', () => { + expect(wrapper.vm.visible).toBe(true) + }) + + it('sets editContributionLink to true', () => { + expect(wrapper.vm.editContributionLink).toBe(true) + }) + + it('sets contributionLinkData', () => { + expect(wrapper.vm.contributionLinkData).toEqual(mockItems[0]) + }) + + it('hides new contribution button', () => { + expect(wrapper.find('[data-test="new-contribution-link-button"]').exists()).toBe(false) + }) + }) + + describe('closeContributionForm', () => { beforeEach(() => { - wrapper = Wrapper() + wrapper.vm.visible = true + wrapper.vm.editContributionLink = true + wrapper.vm.contributionLinkData = mockItems[0] + wrapper.vm.closeContributionForm() }) - it('renders the Div Element ".contribution-link"', () => { - expect(wrapper.find('div.contribution-link').exists()).toBe(true) + it('hides the form', () => { + expect(wrapper.vm.visible).toBe(false) }) - it('has one contribution link in table', () => { - expect(wrapper.find('div.contribution-link-list').find('tbody').findAll('tr')).toHaveLength(1) + it('resets editContributionLink', () => { + expect(wrapper.vm.editContributionLink).toBe(false) }) - it('has contribution form not visible by default', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(false) - }) - - describe('click on create new contribution', () => { - beforeEach(async () => { - await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click') - }) - - it('shows the contribution form', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(true) - }) - - describe('click on create new contribution again', () => { - beforeEach(async () => { - await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click') - }) - - it('closes the contribution form', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(false) - }) - }) - - describe('click on close button', () => { - beforeEach(async () => { - await wrapper.find('button.btn-secondary').trigger('click') - }) - - it('closes the contribution form', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(false) - }) - }) - }) - - describe('edit contribution link', () => { - beforeEach(async () => { - await wrapper - .find('div.contribution-link-list') - .find('tbody') - .findAll('tr') - .at(0) - .findAll('button') - .at(1) - .trigger('click') - }) - - it('shows the contribution form', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(true) - }) - - it('does not show the new contribution button', () => { - expect(wrapper.find('[data-test="new-contribution-link-button"]').exists()).toBe(false) - }) - - describe('click on close button', () => { - beforeEach(async () => { - await wrapper.find('button.btn-secondary').trigger('click') - }) - - it('closes the contribution form', () => { - expect(wrapper.find('#newContribution').isVisible()).toBe(false) - }) - }) + it('resets contributionLinkData', () => { + expect(wrapper.vm.contributionLinkData).toEqual({}) }) }) }) diff --git a/admin/src/components/ContributionLink/ContributionLinkForm.spec.js b/admin/src/components/ContributionLink/ContributionLinkForm.spec.js index 93f39a3fd..ce47f5597 100644 --- a/admin/src/components/ContributionLink/ContributionLinkForm.spec.js +++ b/admin/src/components/ContributionLink/ContributionLinkForm.spec.js @@ -1,144 +1,153 @@ import { mount } from '@vue/test-utils' -import ContributionLinkForm from './ContributionLinkForm' -import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup' -import { createContributionLink } from '@/graphql/createContributionLink.js' +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import ContributionLinkForm from './ContributionLinkForm.vue' -const localVue = global.localVue +// Mock external dependencies +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key) => key, + }), +})) -global.alert = jest.fn() +const mockMutate = vi.fn() +vi.mock('@vue/apollo-composable', () => ({ + useMutation: () => ({ + mutate: mockMutate, + }), +})) -const propsData = { - contributionLinkData: {}, - editContributionLink: false, +const mockToastError = vi.fn() +const mockToastSuccess = vi.fn() +vi.mock('@/composables/useToast', () => ({ + useAppToast: () => ({ + toastError: mockToastError, + toastSuccess: mockToastSuccess, + }), +})) + +const mockRouter = { + push: vi.fn(), } -const apolloMutateMock = jest.fn().mockResolvedValue() - -const mocks = { - $t: jest.fn((t) => t), - $apollo: { - mutate: apolloMutateMock, - }, -} - -// const mockAPIcall = jest.fn() +vi.mock('vue-router', () => ({ + useRouter: () => mockRouter, +})) describe('ContributionLinkForm', () => { let wrapper - const Wrapper = () => { - return mount(ContributionLinkForm, { localVue, mocks, propsData }) + const createWrapper = (props = {}) => { + return mount(ContributionLinkForm, { + props: { + contributionLinkData: {}, + editContributionLink: false, + ...props, + }, + global: { + mocks: { + $t: (key) => key, + }, + stubs: { + BForm: true, + BRow: true, + BCol: true, + BFormGroup: true, + BFormInput: true, + BFormTextarea: true, + BFormSelect: true, + BButton: true, + }, + }, + }) } - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() - }) + beforeEach(() => { + wrapper = createWrapper() + }) - it('renders the Div Element ".contribution-link-form"', () => { - expect(wrapper.find('div.contribution-link-form').exists()).toBe(true) - }) + afterEach(() => { + vi.clearAllMocks() + }) - describe('call onReset', () => { - it('form has the set data', () => { - beforeEach(() => { - wrapper.setData({ - form: { - name: 'name', - memo: 'memo', - amount: 100, - validFrom: 'validFrom', - validTo: 'validTo', - cycle: 'ONCE', - maxPerCycle: 1, - maxAmountPerMonth: 100, - }, - }) - wrapper.vm.onReset() - }) - expect(wrapper.vm.form).toEqual({ - amount: null, - cycle: 'ONCE', - validTo: null, - maxAmountPerMonth: '0', - memo: null, - name: null, - maxPerCycle: 1, - validFrom: null, - }) - }) - }) + it('renders the Div Element ".contribution-link-form"', () => { + expect(wrapper.find('div.contribution-link-form').exists()).toBe(true) + }) - describe('call onSubmit', () => { - it('response with the contribution link url', () => { - wrapper.vm.onSubmit() - }) - }) - - describe('successfull submit', () => { - beforeEach(async () => { - apolloMutateMock.mockResolvedValue({ - data: { - createContributionLink: { - link: 'https://localhost/redeem/CL-1a2345678', - }, - }, - }) - await wrapper - .findAllComponents({ name: 'BFormDatepicker' }) - .at(0) - .vm.$emit('input', '2022-6-18') - await wrapper - .findAllComponents({ name: 'BFormDatepicker' }) - .at(1) - .vm.$emit('input', '2022-7-18') - await wrapper.find('input.test-name').setValue('test name') - await wrapper.find('textarea.test-memo').setValue('test memo') - await wrapper.find('input.test-amount').setValue('100') - await wrapper.find('form').trigger('submit') - }) - - it('calls the API', () => { - expect(apolloMutateMock).toHaveBeenCalledWith({ - mutation: createContributionLink, - variables: { - validFrom: '2022-6-18', - validTo: '2022-7-18', - name: 'test name', - amount: '100', - memo: 'test memo', - cycle: 'ONCE', - maxPerCycle: 1, - maxAmountPerMonth: '0', - id: null, - }, - }) - }) - - it('toasts a succes message', () => { - expect(toastSuccessSpy).toBeCalledWith('https://localhost/redeem/CL-1a2345678') - }) - }) - - describe('send createContributionLink with error', () => { - beforeEach(async () => { - apolloMutateMock.mockRejectedValue({ message: 'OUCH!' }) - await wrapper - .findAllComponents({ name: 'BFormDatepicker' }) - .at(0) - .vm.$emit('input', '2022-6-18') - await wrapper - .findAllComponents({ name: 'BFormDatepicker' }) - .at(1) - .vm.$emit('input', '2022-7-18') - await wrapper.find('input.test-name').setValue('test name') - await wrapper.find('textarea.test-memo').setValue('test memo') - await wrapper.find('input.test-amount').setValue('100') - await wrapper.find('form').trigger('submit') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('OUCH!') + describe('onReset', () => { + it('resets the form data', async () => { + wrapper.vm.form = { + name: 'name', + memo: 'memo', + amount: 100, + validFrom: 'validFrom', + validTo: 'validTo', + cycle: 'ONCE', + maxPerCycle: 1, + maxAmountPerMonth: 100, + } + await wrapper.vm.$nextTick() + await wrapper.vm.onReset() + expect(wrapper.vm.form).toEqual({ + validTo: null, + validFrom: null, }) }) }) + + describe('onSubmit', () => { + const validFormData = { + validFrom: '2022-6-18', + validTo: '2022-7-18', + name: 'test name', + memo: 'test memo', + amount: '100', + cycle: 'ONCE', + maxPerCycle: 1, + maxAmountPerMonth: '0', + } + + beforeEach(async () => { + wrapper.vm.form = validFormData + }) + + it('calls the API and toasts success message on successful submission', async () => { + mockMutate.mockResolvedValue({ + data: { + createContributionLink: { + link: 'https://localhost/redeem/CL-1a2345678', + }, + }, + }) + + await wrapper.vm.onSubmit() + + expect(mockMutate).toHaveBeenCalledWith({ + ...validFormData, + id: null, + }) + + expect(mockToastSuccess).toHaveBeenCalledWith('https://localhost/redeem/CL-1a2345678') + }) + + it('toasts an error message on API error', async () => { + mockMutate.mockRejectedValue({ message: 'OUCH!' }) + + await wrapper.vm.onSubmit() + + expect(mockToastError).toHaveBeenCalledWith('OUCH!') + }) + + it('shows error when validFrom is not set', async () => { + wrapper.vm.form = { ...validFormData, validFrom: null } + await wrapper.vm.$nextTick() + await wrapper.vm.onSubmit() + expect(mockToastError).toHaveBeenCalledWith('contributionLink.noStartDate') + }) + + it('shows error when validTo is not set', async () => { + wrapper.vm.form = { ...validFormData, validTo: null } + await wrapper.vm.$nextTick() + await wrapper.vm.onSubmit() + expect(mockToastError).toHaveBeenCalledWith('contributionLink.noEndDate') + }) + }) }) diff --git a/admin/src/components/ContributionLink/ContributionLinkList.spec.js b/admin/src/components/ContributionLink/ContributionLinkList.spec.js index de4798d41..4c1faaabe 100644 --- a/admin/src/components/ContributionLink/ContributionLinkList.spec.js +++ b/admin/src/components/ContributionLink/ContributionLinkList.spec.js @@ -1,147 +1,122 @@ import { mount } from '@vue/test-utils' -import ContributionLinkList from './ContributionLinkList' -import { toastSuccessSpy, toastErrorSpy } from '../../../test/testSetup' -// import { deleteContributionLink } from '../graphql/deleteContributionLink' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import ContributionLinkList from './ContributionLinkList.vue' +import { BButton, BCard, BCardText, BModal, BTable } from 'bootstrap-vue-next' +import * as apolloComposable from '@vue/apollo-composable' -const localVue = global.localVue +vi.mock('vue-i18n', () => ({ + useI18n: vi.fn(() => ({ + t: (key) => key, + d: (date) => date.toISOString(), + })), +})) -const mockAPIcall = jest.fn() +vi.mock('@vue/apollo-composable', () => ({ + useMutation: vi.fn(() => ({ + mutate: vi.fn(), + })), +})) -const mocks = { - $t: jest.fn((t) => t), - $d: jest.fn((d) => d), - $apollo: { - mutate: mockAPIcall, - }, -} - -const propsData = { - items: [ - { - id: 1, - name: 'Meditation', - memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', - amount: '200', - validFrom: '2022-04-01', - validTo: '2022-08-01', - cycle: 'täglich', - maxPerCycle: '3', - maxAmountPerMonth: 0, - link: 'https://localhost/redeem/CL-1a2345678', - }, - ], -} +// Mock useAppToast +const mockToastError = vi.fn() +const mockToastSuccess = vi.fn() +vi.mock('@/composables/useToast', () => ({ + useAppToast: vi.fn(() => ({ + toastError: mockToastError, + toastSuccess: mockToastSuccess, + })), +})) describe('ContributionLinkList', () => { let wrapper + let mutateMock - const Wrapper = () => { - return mount(ContributionLinkList, { localVue, mocks, propsData }) + const createWrapper = () => { + return mount(ContributionLinkList, { + props: { + items: [ + { + id: 1, + name: 'Meditation', + memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', + amount: '200', + validFrom: '2022-04-01', + validTo: '2022-08-01', + cycle: 'täglich', + maxPerCycle: '3', + maxAmountPerMonth: 0, + link: 'https://localhost/redeem/CL-1a2345678', + }, + ], + }, + global: { + components: { + BTable, + BButton, + BModal, + BCard, + BCardText, + }, + stubs: { + IBiTrash: true, + IBiPencil: true, + IBiEye: true, + FigureQrCode: true, + }, + }, + }) } - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() + beforeEach(() => { + vi.clearAllMocks() + mutateMock = vi.fn() + vi.spyOn(apolloComposable, 'useMutation').mockReturnValue({ mutate: mutateMock }) + wrapper = createWrapper() + }) + + it('renders the Div Element ".contribution-link-list"', () => { + expect(wrapper.find('div.contribution-link-list').exists()).toBe(true) + }) + + it('renders table with contribution link', () => { + expect(wrapper.findComponent({ name: 'BTable' }).exists()).toBe(true) + }) + + describe('edit contribution link', () => { + it('emits editContributionLinkData', async () => { + await wrapper.vm.editContributionLink({ id: 1 }) + expect(wrapper.emitted('edit-contribution-link-data')).toBeTruthy() }) + }) - it('renders the Div Element ".contribution-link-list"', () => { - expect(wrapper.find('div.contribution-link-list').exists()).toBe(true) - }) - - it('renders table with contribution link', () => { - expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain( - 'Meditation', - ) - }) - - describe('edit contribution link', () => { - beforeEach(() => { - wrapper.vm.editContributionLink() - }) - - it('emits editContributionLinkData', async () => { - expect(wrapper.vm.$emit('editContributionLinkData')).toBeTruthy() - }) - }) - - describe('delete contribution link', () => { - let spy - + describe('delete contribution link', () => { + describe('with success', () => { beforeEach(async () => { - jest.clearAllMocks() - wrapper.vm.deleteContributionLink() + mutateMock.mockResolvedValue({}) + await wrapper.vm.handleDelete({ item: { id: 1, name: 'Test' } }) + await wrapper.vm.executeDelete() }) - describe('with success', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve('some value')) - mockAPIcall.mockResolvedValue() - await wrapper.find('.test-delete-link').trigger('click') - }) - - it('opens the modal ', () => { - expect(spy).toBeCalled() - }) - - it.skip('calls the API', () => { - // expect(mockAPIcall).toBeCalledWith( - // expect.objectContaining({ - // mutation: deleteContributionLink, - // variables: { - // id: 1, - // }, - // }), - // ) - }) - - it('toasts a success message', () => { - expect(toastSuccessSpy).toBeCalledWith('contributionLink.deleted') - }) + it('calls the mutation and emits events', async () => { + expect(mutateMock).toHaveBeenCalledWith({ id: 1 }) + expect(wrapper.emitted('close-contribution-form')).toBeTruthy() + expect(wrapper.emitted('get-contribution-links')).toBeTruthy() }) - describe('with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve('some value')) - mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' }) - await wrapper.find('.test-delete-link').trigger('click') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Something went wrong :(') - }) - }) - - describe('cancel delete', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(false)) - mockAPIcall.mockResolvedValue() - await wrapper.find('.test-delete-link').trigger('click') - }) - - it('does not call the API', () => { - expect(mockAPIcall).not.toBeCalled() - }) + it('toasts a success message', () => { + expect(mockToastSuccess).toHaveBeenCalledWith('contributionLink.deleted') }) }) - describe('onClick showButton', () => { - it('modelData contains contribution link', () => { - wrapper.find('button.test-show').trigger('click') - expect(wrapper.vm.modalData).toEqual({ - amount: '200', - cycle: 'täglich', - id: 1, - link: 'https://localhost/redeem/CL-1a2345678', - maxAmountPerMonth: 0, - maxPerCycle: '3', - memo: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l', - name: 'Meditation', - validFrom: '2022-04-01', - validTo: '2022-08-01', - }) + describe('with error', () => { + beforeEach(async () => { + mutateMock.mockRejectedValue(new Error('Something went wrong :(')) + await wrapper.vm.handleDelete({ item: { id: 1, name: 'Test' } }) + await wrapper.vm.executeDelete() + }) + + it('toasts an error message', () => { + expect(mockToastError).toHaveBeenCalledWith('Something went wrong :(') }) }) }) diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js index bb12d0ce7..a64a7dfe9 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js +++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js @@ -1,247 +1,161 @@ import { mount } from '@vue/test-utils' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { nextTick } from 'vue' import ContributionMessagesFormular from './ContributionMessagesFormular' -import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup' -import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage' -import { adminUpdateContribution } from '@/graphql/adminUpdateContribution' +import { BButton, BForm } from 'bootstrap-vue-next' -const localVue = global.localVue +const mockToastError = vi.fn() +vi.mock('@/composables/useToast', () => ({ + useAppToast: () => ({ + toastError: mockToastError, + toastSuccess: vi.fn(), + }), +})) -const apolloMutateMock = jest.fn().mockResolvedValue() +const mockMutate = vi.fn().mockResolvedValue({}) +vi.mock('@vue/apollo-composable', () => ({ + useMutation: () => ({ + mutate: mockMutate, + }), +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key) => key, + }), +})) + +const mockChildComponents = { + BForm, + BFormGroup: { template: '
' }, + BFormCheckbox: { template: '
' }, + BFormInput: { template: '' }, + BTabs: { template: '
' }, + BTab: { template: '
' }, + BTooltip: { template: '
' }, + BFormTextarea: { template: '' }, + BRow: { template: '
' }, + BCol: { template: '
' }, + BButton, + TimePicker: { template: '
' }, +} describe('ContributionMessagesFormular', () => { let wrapper - const propsData = { - contributionId: 42, - contributionMemo: 'It is a test memo', - hideResubmission: true, - } - - const mocks = { - $t: jest.fn((t) => t), - $apollo: { - mutate: apolloMutateMock, - }, - $i18n: { - locale: 'en', - }, - } - - const Wrapper = () => { + const createWrapper = (props = {}) => { return mount(ContributionMessagesFormular, { - localVue, - mocks, - propsData, + global: { + components: mockChildComponents, + mocks: { + $route: { + params: { id: '1' }, + }, + }, + }, + props: { + contributionId: 42, + contributionMemo: 'It is a test memo', + hideResubmission: true, + ...props, + }, }) } - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() - jest.clearAllMocks() - }) + beforeEach(() => { + vi.clearAllMocks() + }) - it('has a DIV .contribution-messages-formular', () => { - expect(wrapper.find('div.contribution-messages-formular').exists()).toBe(true) - }) + it('renders the component', () => { + wrapper = createWrapper() + expect(wrapper.find('.contribution-messages-formular').exists()).toBe(true) + }) - describe('on trigger reset', () => { - beforeEach(async () => { - wrapper.setData({ - form: { - text: 'text form message', - }, - }) - await wrapper.find('form').trigger('reset') - }) + it('resets form on reset event', async () => { + wrapper = createWrapper() + wrapper.vm.form.text = 'text form message' + wrapper.vm.form.memo = 'changed memo' - it('form has empty text and memo reset to contribution memo input', () => { - expect(wrapper.vm.form).toEqual({ - text: '', - memo: 'It is a test memo', - }) - }) - }) - - describe('on trigger submit', () => { - beforeEach(async () => { - wrapper.setData({ - form: { - text: 'text form message', - }, - }) - await wrapper.find('form').trigger('submit') - }) - - it('emitted "get-list-contribution-messages" with data', async () => { - expect(wrapper.emitted('get-list-contribution-messages')).toEqual( - expect.arrayContaining([expect.arrayContaining([42])]), - ) - }) - - it('emitted "update-status" with data', async () => { - expect(wrapper.emitted('update-status')).toEqual( - expect.arrayContaining([expect.arrayContaining([42])]), - ) - }) - }) - - describe('send DIALOG contribution message with success', () => { - beforeEach(async () => { - await wrapper.setData({ - form: { - text: 'text form message', - }, - }) - await wrapper.find('button[data-test="submit-dialog"]').trigger('click') - }) - - it('moderatorMessage has `DIALOG`', () => { - expect(apolloMutateMock).toBeCalledWith({ - mutation: adminCreateContributionMessage, - variables: { - contributionId: 42, - message: 'text form message', - messageType: 'DIALOG', - resubmissionAt: null, - }, - }) - }) - - it('toasts an success message', () => { - expect(toastSuccessSpy).toBeCalledWith('message.request') - }) - }) - - describe('send MODERATOR contribution message with success', () => { - beforeEach(async () => { - await wrapper.setData({ - form: { - text: 'text form message', - }, - }) - - // choose tab - // tabs: text | moderator | memo - // 0 | 1 | 2 - await wrapper - .find('div[data-test="message-type-tabs"]') - .findAll('.nav-item a') - .at(1) - .trigger('click') - - // click save - await wrapper.find('button[data-test="submit-dialog"]').trigger('click') - }) - - it('moderatorMesage has `MODERATOR`', () => { - expect(apolloMutateMock).toBeCalledWith({ - mutation: adminCreateContributionMessage, - variables: { - contributionId: 42, - message: 'text form message', - messageType: 'MODERATOR', - resubmissionAt: null, - }, - }) - }) - - it('toasts an success message', () => { - expect(toastSuccessSpy).toBeCalledWith('message.request') - }) - }) - - describe('send resubmission contribution message with success', () => { - const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days in milliseconds - - beforeEach(async () => { - await wrapper.setData({ - form: { - text: 'text form message', - }, - showResubmissionDate: true, - resubmissionDate: futureDate, - resubmissionTime: '08:46', - }) - await wrapper.find('button[data-test="submit-dialog"]').trigger('click') - }) - - it('graphql payload contain resubmission date', () => { - const futureDateExactTime = futureDate - futureDateExactTime.setHours(8) - futureDateExactTime.setMinutes(46) - expect(apolloMutateMock).toBeCalledWith({ - mutation: adminCreateContributionMessage, - variables: { - contributionId: 42, - message: 'text form message', - messageType: 'DIALOG', - resubmissionAt: futureDateExactTime.toString(), - }, - }) - }) - - it('toasts an success message', () => { - expect(toastSuccessSpy).toBeCalledWith('message.request') - }) - }) - - describe('set memo', () => { - beforeEach(async () => { - // choose tab - // tabs: text | moderator | memo - // 0 | 1 | 2 - await wrapper - .find('div[data-test="message-type-tabs"]') - .findAll('.nav-item a') - .at(2) - .trigger('click') - - // click save - await wrapper.find('button[data-test="submit-dialog"]').trigger('click') - }) - it('check tabindex value is 2', () => { - expect(wrapper.vm.tabindex).toBe(2) - }) - }) - - describe('update contribution memo from moderator for user created contributions', () => { - beforeEach(async () => { - await wrapper.setData({ - form: { - memo: 'changed memo', - }, - tabindex: 2, - }) - await wrapper.find('button[data-test="submit-dialog"]').trigger('click') - }) - - it('adminUpdateContribution was called with contributionId and updated memo', () => { - expect(apolloMutateMock).toBeCalledWith({ - mutation: adminUpdateContribution, - variables: { - id: 42, - memo: 'changed memo', - resubmissionAt: null, - }, - }) - }) - - it('toasts an success message', () => { - expect(toastSuccessSpy).toBeCalledWith('message.request') - }) - }) - - describe('send contribution message with error', () => { - beforeEach(async () => { - apolloMutateMock.mockRejectedValue({ message: 'OUCH!' }) - wrapper = Wrapper() - await wrapper.find('form').trigger('submit') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('OUCH!') - }) + await wrapper.find('form').trigger('reset') + await nextTick() + expect(wrapper.vm.form).toEqual({ + text: '', + memo: 'It is a test memo', }) }) + + it('submits form and emits events', async () => { + wrapper = createWrapper() + wrapper.vm.form.text = 'text form message' + + await wrapper.find('form').trigger('submit') + await nextTick() + expect(wrapper.emitted('get-list-contribution-messages')).toBeTruthy() + expect(wrapper.emitted('get-list-contribution-messages')[0]).toEqual([42]) + expect(wrapper.emitted('update-status')).toBeTruthy() + expect(wrapper.emitted('update-status')[0]).toEqual([42]) + }) + + it('sends DIALOG contribution message', async () => { + wrapper = createWrapper() + wrapper.vm.form.text = 'text form message' + const onSubmitSpy = vi.spyOn(wrapper.vm, 'onSubmit') + + await wrapper.vm.$nextTick() + + await wrapper.find('button[type="submit"]').trigger('click') + expect(onSubmitSpy).toHaveBeenCalled() + }) + + it('sends MODERATOR contribution message', async () => { + wrapper = createWrapper() + const onSubmitSpy = vi.spyOn(wrapper.vm, 'onSubmit') + + wrapper.vm.form.text = 'text form message' + wrapper.vm.tabindex = 1 + await wrapper.vm.$nextTick() + + await wrapper.find('button[type="submit"]').trigger('click') + expect(onSubmitSpy).toHaveBeenCalled() + }) + + it('sends resubmission contribution message', async () => { + const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + wrapper = createWrapper() + const onSubmitSpy = vi.spyOn(wrapper.vm, 'onSubmit') + wrapper.vm.form.text = 'text form message' + wrapper.vm.showResubmissionDate = true + wrapper.vm.resubmissionDate = futureDate + wrapper.vm.resubmissionTime = '08:46' + + await wrapper.vm.$nextTick() + + await wrapper.find('button[type="submit"]').trigger('click') + expect(onSubmitSpy).toHaveBeenCalled() + }) + + it('updates contribution memo', async () => { + wrapper = createWrapper() + const onSubmitSpy = vi.spyOn(wrapper.vm, 'onSubmit') + + wrapper.vm.form.memo = 'changed memo' + wrapper.vm.tabindex = 2 + + await wrapper.vm.$nextTick() + + await wrapper.find('button[type="submit"]').trigger('click') + await nextTick() + expect(onSubmitSpy).toHaveBeenCalled() + }) + + it('handles error when sending contribution message', async () => { + const mockError = new Error('OUCH!') + wrapper = createWrapper() + + mockMutate.mockRejectedValue(mockError) + await wrapper.find('form').trigger('submit') + await nextTick() + + expect(mockToastError).toHaveBeenCalledWith('OUCH!') + }) }) diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js index fe91abe6c..7d87082d4 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js +++ b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js @@ -1,170 +1,167 @@ import { mount } from '@vue/test-utils' -import ContributionMessagesList from './ContributionMessagesList' -import VueApollo from 'vue-apollo' -import { createMockClient } from 'mock-apollo-client' -import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js' -import { toastErrorSpy } from '../../../test/testSetup' +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { ref } from 'vue' +import ContributionMessagesList from './ContributionMessagesList.vue' +import { useQuery } from '@vue/apollo-composable' +import { useAppToast } from '@/composables/useToast' +import { BContainer } from 'bootstrap-vue-next' -const mockClient = createMockClient() -const apolloProvider = new VueApollo({ - defaultClient: mockClient, -}) - -const localVue = global.localVue - -localVue.use(VueApollo) - -const defaultData = () => { +vi.mock('vue', async () => { + const actual = await vi.importActual('vue') return { - adminListContributionMessages: { - count: 4, - messages: [ - { - id: 43, - message: 'A DIALOG message', - createdAt: new Date().toString(), - updatedAt: null, - type: 'DIALOG', - userFirstName: 'Peter', - userLastName: 'Lustig', - userId: 1, - isModerator: true, - }, - { - id: 44, - message: 'Another DIALOG message', - createdAt: new Date().toString(), - updatedAt: null, - type: 'DIALOG', - userFirstName: 'Bibi', - userLastName: 'Bloxberg', - userId: 2, - isModerator: false, - }, - { - id: 45, - message: `DATE ---- -A HISTORY message ---- -AMOUNT`, - createdAt: new Date().toString(), - updatedAt: null, - type: 'HISTORY', - userFirstName: 'Bibi', - userLastName: 'Bloxberg', - userId: 2, - isModerator: false, - }, - { - id: 46, - message: 'A MODERATOR message', - createdAt: new Date().toString(), - updatedAt: null, - type: 'MODERATOR', - userFirstName: 'Peter', - userLastName: 'Lustig', - userId: 1, - isModerator: true, - }, - ], - }, + ...actual, + ref: vi.fn(actual.ref), } +}) +vi.mock('@vue/apollo-composable') +vi.mock('@/composables/useToast') + +const defaultData = { + adminListContributionMessages: { + count: 4, + messages: [ + { + id: 43, + message: 'A DIALOG message', + createdAt: new Date().toString(), + updatedAt: null, + type: 'DIALOG', + userFirstName: 'Peter', + userLastName: 'Lustig', + userId: 1, + isModerator: true, + }, + { + id: 44, + message: 'Another DIALOG message', + createdAt: new Date().toString(), + updatedAt: null, + type: 'DIALOG', + userFirstName: 'Bibi', + userLastName: 'Bloxberg', + userId: 2, + isModerator: false, + }, + { + id: 45, + message: `DATE\n---\nA HISTORY message\n---\nAMOUNT`, + createdAt: new Date().toString(), + updatedAt: null, + type: 'HISTORY', + userFirstName: 'Bibi', + userLastName: 'Bloxberg', + userId: 2, + isModerator: false, + }, + { + id: 46, + message: 'A MODERATOR message', + createdAt: new Date().toString(), + updatedAt: null, + type: 'MODERATOR', + userFirstName: 'Peter', + userLastName: 'Lustig', + userId: 1, + isModerator: true, + }, + ], + }, } describe('ContributionMessagesList', () => { let wrapper + let mockMessages + const mockRefetch = vi.fn() + const mockToastError = vi.fn() - const adminListContributionMessagessMock = jest.fn() + beforeEach(async () => { + vi.clearAllMocks() - mockClient.setRequestHandler( - adminListContributionMessages, - adminListContributionMessagessMock - .mockRejectedValueOnce({ message: 'Auaa!' }) - .mockResolvedValue({ data: defaultData() }), - ) + mockMessages = ref([]) + ref.mockReturnValueOnce(mockMessages) - const propsData = { - contributionId: 42, - contributionMemo: 'test memo', - contributionUserId: 108, - contributionStatus: 'PENDING', - hideResubmission: true, - } - - const mocks = { - $t: jest.fn((t) => t), - $d: jest.fn((d) => d), - $n: jest.fn((n) => n), - $i18n: { - locale: 'en', - }, - } - - const Wrapper = () => { - return mount(ContributionMessagesList, { - localVue, - mocks, - propsData, - apolloProvider, - }) - } - - describe('mount', () => { - beforeEach(() => { - jest.clearAllMocks() - wrapper = Wrapper() + useQuery.mockReturnValue({ + onResult: vi.fn((callback) => callback({ result: defaultData })), + onError: vi.fn(), + result: { value: defaultData }, + refetch: mockRefetch, }) - describe('server response for admin list contribution messages is error', () => { - it('toast an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Auaa!') - }) + useAppToast.mockReturnValue({ + toastError: mockToastError, }) - describe('server response is succes', () => { - it('has a DIV .contribution-messages-list', () => { - expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true) - }) - - it('has 4 messages', () => { - expect(wrapper.findAll('div.contribution-messages-list-item')).toHaveLength(4) - }) - - it('has a Component ContributionMessagesFormular', () => { - expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true) - }) + wrapper = mount(ContributionMessagesList, { + props: { + contributionId: 42, + contributionMemo: 'test memo', + contributionUserId: 108, + contributionStatus: 'PENDING', + hideResubmission: true, + }, + global: { + components: { + BContainer, + }, + mocks: { + $t: (key) => key, + $d: (date) => date, + $n: (number) => number, + }, + stubs: { + 'contribution-messages-list-item': true, + 'contribution-messages-formular': true, + }, + }, }) - describe('call updateStatus', () => { - beforeEach(() => { - wrapper.vm.updateStatus(4) - }) + await wrapper.vm.$nextTick() + }) - it('emits update-status', () => { - expect(wrapper.vm.$root.$emit('update-status', 4)).toBeTruthy() - }) - }) + afterEach(() => { + wrapper.unmount() + }) - describe('test reload-contribution', () => { - beforeEach(() => { - wrapper.vm.reloadContribution(3) - }) + it('renders the component', () => { + expect(wrapper.find('.contribution-messages-list').exists()).toBe(true) + }) - it('emits reload-contribution', () => { - expect(wrapper.emitted('reload-contribution')).toBeTruthy() - expect(wrapper.emitted('reload-contribution')[0]).toEqual([3]) - }) - }) + it('renders the correct number of messages', async () => { + wrapper.vm.messages = defaultData.adminListContributionMessages.messages + await wrapper.vm.$nextTick() + expect(wrapper.findAll('contribution-messages-list-item-stub')).toHaveLength(4) + }) - describe('test update-contributions', () => { - beforeEach(() => { - wrapper.vm.updateContributions() - }) + it('renders the ContributionMessagesFormular when status is PENDING', () => { + expect(wrapper.find('contribution-messages-formular-stub').exists()).toBe(true) + }) - it('emits update-contributions', () => { - expect(wrapper.emitted('update-contributions')).toBeTruthy() - }) - }) + it('does not render the ContributionMessagesFormular when status is not PENDING or IN_PROGRESS', async () => { + await wrapper.setProps({ contributionStatus: 'COMPLETED' }) + expect(wrapper.find('contribution-messages-formular-stub').exists()).toBe(false) + }) + + it('updates messages when result changes', async () => { + const newMessages = [{ id: 1, message: 'New message' }] + mockMessages.value = newMessages + await wrapper.vm.$nextTick() + expect(wrapper.findAll('contribution-messages-list-item-stub')).toHaveLength(1) + }) + + it('emits update-status event', async () => { + await wrapper.vm.updateStatus(4) + expect(wrapper.emitted('update-status')).toBeTruthy() + expect(wrapper.emitted('update-status')[0]).toEqual([4]) + }) + + it('emits reload-contribution event', async () => { + await wrapper.vm.reloadContribution(3) + expect(wrapper.emitted('reload-contribution')).toBeTruthy() + expect(wrapper.emitted('reload-contribution')[0]).toEqual([3]) + }) + + it('emits update-contributions event', async () => { + await wrapper.vm.updateContributions() + expect(wrapper.emitted('update-contributions')).toBeTruthy() }) }) diff --git a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js index 637c470be..bcd6ddd83 100644 --- a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js +++ b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js @@ -1,260 +1,155 @@ import { mount } from '@vue/test-utils' -import ContributionMessagesListItem from './ContributionMessagesListItem' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import ContributionMessagesListItem from './ContributionMessagesListItem.vue' -const localVue = global.localVue +vi.mock('@/components/ContributionMessages/ParseMessage', () => ({ + default: { + name: 'ParseMessage', + template: '
{{ message }}
', + props: ['message'], + }, +})) -const dateMock = jest.fn((d) => d) -const numberMock = jest.fn((n) => n) - -describe('ContributionMessagesListItem', () => { - let wrapper - - const mocks = { - $t: jest.fn((t) => t), - $d: dateMock, - $n: numberMock, - $store: { - state: { - moderator: { - firstName: 'Peter', - lastName: 'Lustig', +const createWrapper = (propsData) => { + return mount(ContributionMessagesListItem, { + props: propsData, + global: { + mocks: { + $t: (key) => key, + $d: vi.fn((date) => date.toISOString()), + $n: vi.fn((n) => n.toString()), + $store: { + state: { + moderator: { + firstName: 'Peter', + lastName: 'Lustig', + }, + }, }, }, - }, - } - - describe('if message author has moderator role', () => { - const propsData = { - contributionId: 42, - contributionUserId: 108, - state: 'PENDING', - message: { - id: 111, - message: 'Lorem ipsum?', - createdAt: '2022-08-29T12:23:27.000Z', - updatedAt: null, - type: 'DIALOG', - userFirstName: 'Peter', - userLastName: 'Lustig', - userId: 107, - isModerator: true, - __typename: 'ContributionMessage', + stubs: { + BAvatar: true, + VariantIcon: true, }, - } + }, + }) +} - const ModeratorItemWrapper = () => { - return mount(ContributionMessagesListItem, { - localVue, - mocks, - propsData, - }) - } +describe('ContributionMessagesListItem', () => { + describe('if message author has moderator role', () => { + let wrapper - describe('mount', () => { - beforeAll(() => { - wrapper = ModeratorItemWrapper() + beforeEach(() => { + wrapper = createWrapper({ + contributionUserId: 108, + message: { + id: 111, + message: 'Lorem ipsum?', + createdAt: '2022-08-29T12:23:27.000Z', + updatedAt: null, + type: 'DIALOG', + userFirstName: 'Peter', + userLastName: 'Lustig', + userId: 107, + isModerator: true, + }, }) + }) - it('has a DIV .text-end.is-moderator', () => { - expect(wrapper.find('div.text-end.is-moderator').exists()).toBe(true) - }) + it('has a DIV .text-end.is-moderator', () => { + expect(wrapper.find('div.text-end.is-moderator').exists()).toBe(true) + }) - it('has the complete user name', () => { - expect(wrapper.find('[data-test="moderator-name"]').text()).toBe('Peter Lustig') - }) + it('has the complete user name', () => { + expect(wrapper.find('[data-test="moderator-name"]').text()).toBe('Peter Lustig') + }) - it('has the message creation date', () => { - expect(wrapper.find('[data-test="moderator-date"]').text()).toMatch( - 'Mon Aug 29 2022 12:23:27 GMT+0000', - ) - }) + it('has the message creation date', () => { + expect(wrapper.find('[data-test="moderator-date"]').text()).toBe('2022-08-29T12:23:27.000Z') + }) - it('has the moderator label', () => { - expect(wrapper.find('[data-test="moderator-label"]').text()).toBe('moderator.moderator') - }) + it('has the moderator label', () => { + expect(wrapper.find('[data-test="moderator-label"]').text()).toBe('moderator.moderator') + }) - it('has the message', () => { - expect(wrapper.find('[data-test="moderator-message"]').text()).toBe('Lorem ipsum?') - }) + it('has the message', () => { + expect(wrapper.find('[data-test="moderator-message"]').text()).toBe('Lorem ipsum?') }) }) describe('if message author does not have moderator role', () => { - const propsData = { - contributionId: 42, - contributionUserId: 108, - state: 'PENDING', - message: { - id: 113, - message: 'Asda sdad ad asdasd, das Ass das Das. ', - createdAt: '2022-08-29T12:25:34.000Z', - updatedAt: null, - type: 'DIALOG', - userFirstName: 'Bibi', - userLastName: 'Bloxberg', - userId: 108, - __typename: 'ContributionMessage', - }, - } + let wrapper - const ItemWrapper = () => { - return mount(ContributionMessagesListItem, { - localVue, - mocks, - propsData, - }) - } - - describe('mount', () => { - beforeAll(() => { - wrapper = ItemWrapper() - }) - - it('has a DIV .text-start.is-not-moderator', () => { - expect(wrapper.find('div.text-start.is-user').exists()).toBe(true) - }) - - it('has the complete user name', () => { - expect(wrapper.find('[data-test="user-name"]').text()).toBe('Bibi Bloxberg') - }) - - it('has the message creation date', () => { - expect(wrapper.find('[data-test="user-date"]').text()).toMatch( - 'Mon Aug 29 2022 12:25:34 GMT+0000', - ) - }) - - it('has the message', () => { - expect(wrapper.find('[data-test="user-message"]').text()).toBe( - 'Asda sdad ad asdasd, das Ass das Das.', - ) - }) - }) - }) - - describe('links in contribtion message', () => { - const propsData = { - contributionUserId: 108, - message: { - id: 111, - message: 'Lorem ipsum?', - createdAt: '2022-08-29T12:23:27.000Z', - updatedAt: null, - type: 'DIALOG', - userFirstName: 'Peter', - userLastName: 'Lustig', - userId: 107, - __typename: 'ContributionMessage', - }, - } - - const ModeratorItemWrapper = () => { - return mount(ContributionMessagesListItem, { - localVue, - mocks, - propsData, - }) - } - - let messageField - - describe('message of only one link', () => { - beforeEach(() => { - propsData.message.message = 'https://gradido.net/de/' - wrapper = ModeratorItemWrapper() - messageField = wrapper.find('[data-test="moderator-message"]') - }) - - it('contains the link as text', () => { - expect(messageField.text()).toBe('https://gradido.net/de/') - }) - - it('contains a link to the given address', () => { - expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/') + beforeEach(() => { + wrapper = createWrapper({ + contributionUserId: 108, + message: { + id: 113, + message: 'Asda sdad ad asdasd, das Ass das Das.', + createdAt: '2022-08-29T12:25:34.000Z', + updatedAt: null, + type: 'DIALOG', + userFirstName: 'Bibi', + userLastName: 'Bloxberg', + userId: 108, + }, }) }) - describe('message with text and two links', () => { - beforeEach(() => { - propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/ -and here is the link to the repository: https://github.com/gradido/gradido` - wrapper = ModeratorItemWrapper() - messageField = wrapper.find('[data-test="moderator-message"]') - }) + it('has a DIV .text-start.is-user', () => { + expect(wrapper.find('div.text-start.is-user').exists()).toBe(true) + }) - it('contains the whole text', () => { - expect(messageField.text()) - .toBe(`Here you find all you need to know about Gradido: https://gradido.net/de/ -and here is the link to the repository: https://github.com/gradido/gradido`) - }) + it('has the complete user name', () => { + expect(wrapper.find('[data-test="user-name"]').text()).toBe('Bibi Bloxberg') + }) - it('contains the two links', () => { - expect(messageField.findAll('a').at(0).attributes('href')).toBe('https://gradido.net/de/') - expect(messageField.findAll('a').at(1).attributes('href')).toBe( - 'https://github.com/gradido/gradido', - ) - }) + it('has the message creation date', () => { + expect(wrapper.find('[data-test="user-date"]').text()).toBe('2022-08-29T12:25:34.000Z') + }) + + it('has the message', () => { + expect(wrapper.find('[data-test="user-message"]').text()).toBe( + 'Asda sdad ad asdasd, das Ass das Das.', + ) }) }) describe('contribution message type HISTORY', () => { - const propsData = { - contributionUserId: 108, - message: { - id: 111, - message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time) + let wrapper + + beforeEach(() => { + wrapper = createWrapper({ + contributionUserId: 108, + message: { + id: 111, + message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time) --- This message also contains a link: https://gradido.net/de/ --- 350.00`, - createdAt: '2022-08-29T12:23:27.000Z', - updatedAt: null, - type: 'HISTORY', - userFirstName: 'Peter', - userLastName: 'Lustig', - userId: 107, - __typename: 'ContributionMessage', - }, - } - - const itemWrapper = () => { - return mount(ContributionMessagesListItem, { - localVue, - mocks, - propsData, + createdAt: '2022-08-29T12:23:27.000Z', + updatedAt: null, + type: 'HISTORY', + userFirstName: 'Peter', + userLastName: 'Lustig', + userId: 107, + }, }) - } + }) - let messageField + it('renders the history label', () => { + expect(wrapper.text()).toContain('moderator.history') + }) - describe('render HISTORY message', () => { - beforeEach(() => { - jest.clearAllMocks() - wrapper = itemWrapper() - messageField = wrapper - }) - - it('renders the date', () => { - expect(dateMock).toBeCalledWith( - new Date('Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time'), - 'short', - ) - }) - - it('renders the amount', () => { - expect(numberMock).toBeCalledWith(350, 'decimal') - expect(messageField.text()).toContain('350 GDD') - }) - - it('contains the link as text', () => { - expect(messageField.text()).toContain( - 'This message also contains a link: https://gradido.net/de/', - ) - }) - - it('contains a link to the given address', () => { - expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/') - }) + it('renders the message', () => { + expect(wrapper.find('[data-test="moderator-message"]').text()).toContain( + 'Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time)', + ) + expect(wrapper.find('[data-test="moderator-message"]').text()).toContain( + 'This message also contains a link: https://gradido.net/de/', + ) + expect(wrapper.find('[data-test="moderator-message"]').text()).toContain('350.00') }) }) }) diff --git a/admin/src/components/CreationFormular.spec.js b/admin/src/components/CreationFormular.spec.js index 8f8cc03b0..e0ea750b7 100644 --- a/admin/src/components/CreationFormular.spec.js +++ b/admin/src/components/CreationFormular.spec.js @@ -1,364 +1,147 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' -import CreationFormular from './CreationFormular' -import { adminCreateContribution } from '../graphql/adminCreateContribution' -import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' -import VueApollo from 'vue-apollo' -import { createMockClient } from 'mock-apollo-client' -import { adminOpenCreations } from '../graphql/adminOpenCreations' +import { nextTick, ref } from 'vue' +import CreationFormular from './CreationFormular.vue' +import { BFormRadioGroup } from 'bootstrap-vue-next' -const mockClient = createMockClient() -const apolloProvider = new VueApollo({ - defaultClient: mockClient, -}) - -const localVue = global.localVue -localVue.use(VueApollo) - -const stateCommitMock = jest.fn() - -const mocks = { - $t: jest.fn((t, options) => (options ? [t, options] : t)), - $d: jest.fn((d) => { - const date = new Date(d) - return date.toISOString().split('T')[0] +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key) => key, }), - $store: { - commit: stateCommitMock, - }, -} +})) -const propsData = { - type: '', - creation: [], -} +vi.mock('@/composables/useToast', () => ({ + useAppToast: () => ({ + toastError: vi.fn(), + toastSuccess: vi.fn(), + }), +})) -const now = new Date() +vi.mock('@vue/apollo-composable', () => ({ + useMutation: () => ({ + mutate: vi.fn(), + }), + useQuery: () => ({ + refetch: vi.fn(), + }), +})) -const getCreationDate = (sub) => { - const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0) - return date.toISOString().split('T')[0] +vi.mock('vuex', () => ({ + useStore: () => ({ + commit: vi.fn(), + }), +})) + +vi.mock('../composables/useCreationMonths', () => ({ + default: () => ({ + creationDateObjects: ref([ + { short: 'Jan', year: '2024', date: '2024-01-01' }, + { short: 'Feb', year: '2024', date: '2024-02-01' }, + ]), + }), +})) + +const mockChildComponents = { + BForm: { template: '
' }, + BFormRadioGroup, + BInputGroup: { template: '
' }, + BFormInput: { template: '', props: ['modelValue'] }, + BFormTextarea: { template: '', props: ['modelValue'] }, + BButton: { template: '' }, } describe('CreationFormular', () => { let wrapper - const adminOpenCreationsMock = jest.fn() - const adminCreateContributionMock = jest.fn() - mockClient.setRequestHandler( - adminOpenCreations, - adminOpenCreationsMock.mockResolvedValue({ - data: { - adminOpenCreations: [ - { - month: new Date(now.getFullYear(), now.getMonth() - 2).getMonth(), - year: new Date(now.getFullYear(), now.getMonth() - 2).getFullYear(), - amount: '200', - }, - { - month: new Date(now.getFullYear(), now.getMonth() - 1).getMonth(), - year: new Date(now.getFullYear(), now.getMonth() - 1).getFullYear(), - amount: '400', - }, - { - month: now.getMonth(), - year: now.getFullYear(), - amount: '600', - }, - ], + beforeEach(() => { + wrapper = mount(CreationFormular, { + global: { + stubs: mockChildComponents, + mocks: { + $t: (key) => key, + }, }, - }), - ) - mockClient.setRequestHandler( - adminCreateContribution, - adminCreateContributionMock.mockResolvedValue({ - data: { - adminCreateContribution: [0, 0, 0], + props: { + pagetype: '', + item: {}, + items: [], + creationUserData: {}, + creation: [100, 200], // Mock creation data }, - }), - ) - - const Wrapper = () => { - return mount(CreationFormular, { localVue, mocks, propsData, apolloProvider }) - } - - describe('mount', () => { - beforeEach(() => { - wrapper = Wrapper() - }) - - it('has a DIV element with the class.component-creation-formular', () => { - expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy() - }) - - describe('text and value form props', () => { - beforeEach(async () => { - wrapper = mount(CreationFormular, { - localVue, - mocks, - propsData: { - creationUserData: { memo: 'Memo from property', amount: 42 }, - ...propsData, - }, - }) - }) - - it('has text taken from props', () => { - expect(wrapper.vm.text).toBe('Memo from property') - }) - - it('has value taken from props', () => { - expect(wrapper.vm.value).toBe(42) - }) - }) - - describe('radio buttons to selcet month', () => { - it('has three radio buttons', () => { - expect(wrapper.findAll('input[type="radio"]').length).toBe(3) - }) - - describe('with single creation', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ - type: 'singleCreation', - creation: [200, 400, 600], - item: { email: 'benjamin@bluemchen.de' }, - }) - await wrapper.findAll('input[type="radio"]').at(1).setChecked() - await wrapper.find('input[type="number"]').setValue(90) - }) - - describe('first radio button', () => { - beforeEach(async () => { - await wrapper.findAll('input[type="radio"]').at(0).setChecked() - await wrapper.find('textarea').setValue('Test create coins') - }) - - it('sets rangeMax to 200', () => { - expect(wrapper.vm.rangeMax).toBe(200) - }) - - describe('sendForm', () => { - beforeEach(async () => { - await wrapper.find('.test-submit').trigger('click') - }) - - it('sends ... to apollo', () => { - expect(adminCreateContributionMock).toBeCalledWith({ - email: 'benjamin@bluemchen.de', - creationDate: getCreationDate(2), - amount: 90, - memo: 'Test create coins', - }) - }) - - it('emits update-user-data', () => { - expect(wrapper.emitted('update-user-data')).toEqual([ - [{ email: 'benjamin@bluemchen.de' }, [0, 0, 0]], - ]) - }) - - it('toasts a success message', () => { - expect(toastSuccessSpy).toBeCalledWith([ - 'creation_form.toasted', - { email: 'benjamin@bluemchen.de', value: '90' }, - ]) - }) - - it('updates open creations in store', () => { - expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 1) - }) - - it('resets the form data', () => { - expect(wrapper.vm.value).toBe(0) - }) - }) - - describe('sendForm with server error', () => { - beforeEach(async () => { - adminCreateContributionMock.mockRejectedValueOnce({ message: 'Ouch!' }) - await wrapper.find('.test-submit').trigger('click') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Ouch!') - }) - }) - - describe('Negativ value', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ value: -20 }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Empty text', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: '' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Text length less than 10', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: 'Try this' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - }) - - describe('second radio button', () => { - beforeEach(async () => { - await wrapper.findAll('input[type="radio"]').at(1).setChecked() - }) - - it('sets rangeMin to 0', () => { - expect(wrapper.vm.rangeMin).toBe(0) - }) - - it('sets rangeMax to 400', () => { - expect(wrapper.vm.rangeMax).toBe(400) - }) - - describe('sendForm', () => { - beforeEach(async () => { - await wrapper.find('.test-submit').trigger('click') - }) - - it('sends ... to apollo', () => { - expect(adminCreateContributionMock).toBeCalled() - }) - }) - - describe('Negativ value', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ value: -20 }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Empty text', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: '' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Text length less than 10', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: 'Try this' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - }) - - describe('third radio button', () => { - beforeEach(async () => { - await wrapper.findAll('input[type="radio"]').at(2).setChecked() - }) - - it('sets rangeMin to 0', () => { - expect(wrapper.vm.rangeMin).toBe(0) - }) - - it('sets rangeMax to 400', () => { - expect(wrapper.vm.rangeMax).toBe(600) - }) - - describe('sendForm', () => { - beforeEach(async () => { - await wrapper.find('.test-submit').trigger('click') - }) - - it('sends mutation to apollo', () => { - expect(adminCreateContributionMock).toBeCalled() - }) - - it('toast success message', () => { - expect(toastSuccessSpy).toBeCalled() - }) - - it('store commit openCreationPlus', () => { - expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 1) - }) - }) - - describe('Negativ value', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ value: -20 }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Empty text', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: '' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - - describe('Text length less than 10', () => { - beforeEach(async () => { - jest.clearAllMocks() - await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) - await wrapper.setData({ rangeMin: 180 }) - await wrapper.setData({ text: 'Try this' }) - }) - - it('has no submit button', async () => { - expect(await wrapper.find('.test-submit').attributes('disabled')).toBe('disabled') - }) - }) - }) - }) }) }) + + it('renders correctly', () => { + expect(wrapper.exists()).toBe(true) + }) + + it('initializes with default values', () => { + expect(wrapper.vm.text).toBe('') + expect(wrapper.vm.value).toBe(0) + expect(wrapper.vm.selected).toBe(null) + }) + + it('updates radio options based on creationDateObjects', async () => { + await nextTick() + expect(wrapper.vm.radioOptions).toHaveLength(2) + expect(wrapper.vm.radioOptions[0].name).toContain('Jan') + expect(wrapper.vm.radioOptions[1].name).toContain('Feb') + }) + + it('handles month selection', async () => { + const radioGroup = wrapper.findComponent({ name: 'BFormRadioGroup' }) + await radioGroup.vm.$emit('update:modelValue', { + short: 'Jan', + year: '2024', + date: '2024-01-01', + creation: 100, + }) + expect(wrapper.vm.selected).toEqual({ + short: 'Jan', + year: '2024', + date: '2024-01-01', + creation: 100, + }) + expect(wrapper.vm.text).toBe('creation_form.creation_for Jan 2024') + }) + + it('disables submit button when form is invalid', async () => { + wrapper.vm.selected = null + wrapper.vm.value = 0 + wrapper.vm.text = '' + await wrapper.vm.$nextTick() + const submitButton = wrapper.find('.test-submit') + expect(submitButton.attributes('disabled')).toBeDefined() + }) + + it('enables submit button when form is valid', async () => { + wrapper.vm.selected = { short: 'Jan', year: '2024', date: '2024-01-01', creation: 100 } + wrapper.vm.value = 100 + wrapper.vm.text = 'Valid text input' + await wrapper.vm.$nextTick() + const submitButton = wrapper.find('.test-submit') + expect(submitButton.attributes('disabled')).toBeUndefined() + }) + + it('resets form on reset button click', async () => { + wrapper.vm.selected = { short: 'Jan', year: '2024', date: '2024-01-01', creation: 100 } + wrapper.vm.value = 100 + wrapper.vm.text = 'Some text' + await wrapper.vm.$nextTick() + const resetButton = wrapper.find('button[type="reset"]') + await resetButton.trigger('click') + expect(wrapper.vm.selected).toBe(null) + expect(wrapper.vm.value).toBe(0) + expect(wrapper.vm.text).toBe('') + }) + + it('displays different button text based on pagetype', async () => { + await wrapper.setProps({ pagetype: 'PageCreationConfirm' }) + await wrapper.vm.$nextTick() + expect(wrapper.vm.submitBtnText).toBe('creation_form.update_creation') + + await wrapper.setProps({ pagetype: '' }) + await wrapper.vm.$nextTick() + expect(wrapper.vm.submitBtnText).toBe('creation_form.submit_creation') + }) }) diff --git a/admin/src/components/CreationFormular.vue b/admin/src/components/CreationFormular.vue index c976a1997..2f3ba19ce 100644 --- a/admin/src/components/CreationFormular.vue +++ b/admin/src/components/CreationFormular.vue @@ -47,24 +47,13 @@
- {{ $t('creation_form.update_creation') }} - - - {{ $t('creation_form.submit_creation') }} + {{ submitBtnText }}
@@ -128,7 +117,7 @@ const text = ref(!props.creationUserData.memo ? '' : props.creationUserData.memo const value = ref(!props.creationUserData.amount ? 0 : props.creationUserData.amount) const rangeMin = ref(0) const rangeMax = ref(1000) -const selected = ref() +const selected = ref(null) const creationForm = ref(null) const radioOptions = computed(() => { @@ -140,6 +129,16 @@ const radioOptions = computed(() => { }) }) +const disabled = computed(() => { + return selected.value === '' || value.value <= 0 || text.value.length < 10 +}) + +const submitBtnText = computed(() => { + return props.pagetype === 'PageCreationConfirm' + ? t('creation_form.update_creation') + : t('creation_form.submit_creation') +}) + const updateRadioSelected = (name) => { text.value = `${t('creation_form.creation_for')} ${name?.short} ${name?.year}` rangeMin.value = 0 @@ -189,11 +188,13 @@ const submitCreation = async () => { watch( () => selected.value, async (newValue, oldValue) => { - if (newValue !== oldValue && selected.value !== '') { + if (newValue !== oldValue && selected.value !== '' && selected.value !== null) { updateRadioSelected(newValue) } }, ) + +defineExpose({ submitCreation })