diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7000100e..34ebeff11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 68 + min_coverage: 74 token: ${{ github.token }} ########################################################################## diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fdfd07f..d4eb48283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,149 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.13.1](https://github.com/gradido/gradido/compare/1.13.0...1.13.1) + +- Fix: correctly evaluate to EMAIL_TEST_MODE to false [`#2273`](https://github.com/gradido/gradido/pull/2273) +- Refactor: Contribution resolver logs and events [`#2231`](https://github.com/gradido/gradido/pull/2231) + +#### [1.13.0](https://github.com/gradido/gradido/compare/1.12.1...1.13.0) + +> 18 October 2022 + +- release: Version 1.13.0 [`#2269`](https://github.com/gradido/gradido/pull/2269) +- fix: Linked User Email in Transaction List [`#2268`](https://github.com/gradido/gradido/pull/2268) +- concept capturing alias [`#2148`](https://github.com/gradido/gradido/pull/2148) +- fix: 🍰 Daily Redeem Of Contribution Link [`#2265`](https://github.com/gradido/gradido/pull/2265) +- fix: 🐛 Prevent Loosing Redeem Code When Changing Between Register and Login in Auth Navbar [`#2260`](https://github.com/gradido/gradido/pull/2260) +- fix: Disable Change of Month on Update Contribution [`#2264`](https://github.com/gradido/gradido/pull/2264) +- feat: 🍰 Global Jest Extension For Decimal Equal [`#2261`](https://github.com/gradido/gradido/pull/2261) +- feat: 🍰 Daily Rule For Contribution Links In Admin Interface [`#2262`](https://github.com/gradido/gradido/pull/2262) +- feat: 🍰 Do Not Show Expired Contribution Links In Wallet [`#2257`](https://github.com/gradido/gradido/pull/2257) +- fix: 🍰 Disable Change Of Month For Update Contribution (wallet and admin) [`#2258`](https://github.com/gradido/gradido/pull/2258) +- refactor: 🍰 Login And Logout To Mutations [`#2232`](https://github.com/gradido/gradido/pull/2232) +- fix: 🐛 Verify Token Before Redeeming A Link [`#2254`](https://github.com/gradido/gradido/pull/2254) +- Refactor: Add all events to documentation table [`#2240`](https://github.com/gradido/gradido/pull/2240) +- reconfig log4js with rollover feature and userid in logevent-message [`#2221`](https://github.com/gradido/gradido/pull/2221) +- refactor: 🍰 Refactoring Components Of `CotributionMessagesListItem` [`#2251`](https://github.com/gradido/gradido/pull/2251) +- style: add border-radius on send form [`#2233`](https://github.com/gradido/gradido/pull/2233) +- 2198 adminarea more dates on created transaction [`#2212`](https://github.com/gradido/gradido/pull/2212) +- Bug: delete contribution link [`#2213`](https://github.com/gradido/gradido/pull/2213) +- chore: 🍰 Fix Cypress Tests Unreliability [`#2245`](https://github.com/gradido/gradido/pull/2245) +- docs: 🍰 Refine Deployment Documentation [`#2209`](https://github.com/gradido/gradido/pull/2209) +- End-to-end test setup [`#2047`](https://github.com/gradido/gradido/pull/2047) +- config testmodus flag for sending emails to test or team account instead of user account [`#2216`](https://github.com/gradido/gradido/pull/2216) +- GradidoID 1: adapt and migrate database schema [`#2058`](https://github.com/gradido/gradido/pull/2058) +- feat: Add Client Request Time to Context [`#2206`](https://github.com/gradido/gradido/pull/2206) +- 2219 feature rework eventprotocol [`#2234`](https://github.com/gradido/gradido/pull/2234) +- Refactor: Test register with redeem code [`#2214`](https://github.com/gradido/gradido/pull/2214) +- 2203 delete query modal when redeeming the redeem link [`#2211`](https://github.com/gradido/gradido/pull/2211) +- Refactor: 🍰 Change email templates [`#2228`](https://github.com/gradido/gradido/pull/2228) +- Refactor: Events and logs completed in User Resolver [`#2204`](https://github.com/gradido/gradido/pull/2204) +- change support mail [`#2210`](https://github.com/gradido/gradido/pull/2210) +- feat: 🍰 Send email when contribution is confirmed [`#2193`](https://github.com/gradido/gradido/pull/2193) +- feat: 🍰 Send email when admin writes message to contribution [`#2187`](https://github.com/gradido/gradido/pull/2187) +- feat: 🍰 Send Email To Transaction Link Sender After Receiver Redeemed It [`#2063`](https://github.com/gradido/gradido/pull/2063) + +#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1) + +> 13 September 2022 + +- release: Version 1.12.1 [`#2196`](https://github.com/gradido/gradido/pull/2196) +- fix: 🍰 Show Not Icons In `allContribution` List [`#2195`](https://github.com/gradido/gradido/pull/2195) + +#### [1.12.0](https://github.com/gradido/gradido/compare/1.11.0...1.12.0) + +> 12 September 2022 + +- release: v1.12.0 [`#2191`](https://github.com/gradido/gradido/pull/2191) +- if message empty else disabled button [`#2189`](https://github.com/gradido/gradido/pull/2189) +- messages show if Confirmed [`#2185`](https://github.com/gradido/gradido/pull/2185) +- text in messages smaller [`#2186`](https://github.com/gradido/gradido/pull/2186) +- feat: 🍰 Klicktipp retrieve not registered email [`#2181`](https://github.com/gradido/gradido/pull/2181) +- fix: 🍰 isModerator on messages to switch the messages side in the messages overview [`#2182`](https://github.com/gradido/gradido/pull/2182) +- Refactor locales for Nederlands [`#2174`](https://github.com/gradido/gradido/pull/2174) +- Add is moderator to contribution message [`#2180`](https://github.com/gradido/gradido/pull/2180) +- feat: 🍰 Moderator Cannot Answer Himself [`#2178`](https://github.com/gradido/gradido/pull/2178) +- refactor: Improve Statistics Query [`#2170`](https://github.com/gradido/gradido/pull/2170) +- fix: Remove Statistics from Wallet [`#2171`](https://github.com/gradido/gradido/pull/2171) +- feat: 🍰 Contribution Messages In Frontend [`#2164`](https://github.com/gradido/gradido/pull/2164) +- feat: 🚀 CRUD For Contribution Messages [`#2149`](https://github.com/gradido/gradido/pull/2149) +- fix: 🍰 Decay Calculation In Community Statistics [`#2167`](https://github.com/gradido/gradido/pull/2167) +- chore: 🍰 Remove Fetch Policy Network Only From Statistics [`#2159`](https://github.com/gradido/gradido/pull/2159) +- feat: 🍰 Remove Some Statistics Data From Frontend [`#2153`](https://github.com/gradido/gradido/pull/2153) +- feat: 🍰 Add Toogle Collaps On Language Name [`#2156`](https://github.com/gradido/gradido/pull/2156) +- 2145 corrections style for frontend [`#2147`](https://github.com/gradido/gradido/pull/2147) +- 2072 feature usecase contribution messaging [`#2073`](https://github.com/gradido/gradido/pull/2073) +- 2151 add hint to redeem link [`#2158`](https://github.com/gradido/gradido/pull/2158) +- 🍰 Create `contribution messages` table [`#2137`](https://github.com/gradido/gradido/pull/2137) +- feat: 🍰 Add The Languages French And Dutch [`#2138`](https://github.com/gradido/gradido/pull/2138) +- 1973 list open contribution links in the wallet [`#1975`](https://github.com/gradido/gradido/pull/1975) +- feat: 🍰 Admin Interface Displays Statistics [`#2124`](https://github.com/gradido/gradido/pull/2124) +- feat: Statistics Resolver [`#2041`](https://github.com/gradido/gradido/pull/2041) +- 2116 retrieve admin and moderators [`#2127`](https://github.com/gradido/gradido/pull/2127) +- 2125 feature gradido id: new column gradidoid in users table [`#2126`](https://github.com/gradido/gradido/pull/2126) +- 2119 new menu item gdt [`#2120`](https://github.com/gradido/gradido/pull/2120) +- feat: Migrate Contributions Table [`#2136`](https://github.com/gradido/gradido/pull/2136) +- chore: 🍰 Refactor Contribution Form Logic And Write Tests [`#2092`](https://github.com/gradido/gradido/pull/2092) +- fix: 🍰 Add `emailChecked` Before Changing `optIn` State & Log Error On klicktipp Middleware [`#2107`](https://github.com/gradido/gradido/pull/2107) +- Add RIGHTS.LIST_CONTRIBUTION_LINKS to ROLE_USER [`#2123`](https://github.com/gradido/gradido/pull/2123) +- 2121 translate locales to spanish [`#2122`](https://github.com/gradido/gradido/pull/2122) +- add formatter on input amount replace point and comma [`#2115`](https://github.com/gradido/gradido/pull/2115) +- remove required from form.memo [`#2114`](https://github.com/gradido/gradido/pull/2114) +- Fix pagination ellipsis [`#2104`](https://github.com/gradido/gradido/pull/2104) + +#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0) + +> 28 July 2022 + +- release: Version 1.11.0 [`#2103`](https://github.com/gradido/gradido/pull/2103) +- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102) +- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101) +- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099) +- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100) +- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098) +- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053) +- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090) +- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093) +- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086) +- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088) +- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087) +- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085) +- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083) +- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067) +- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042) +- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082) +- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080) +- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050) +- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064) +- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066) +- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062) +- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035) +- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061) +- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022) +- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997) +- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060) +- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057) +- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059) +- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052) +- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048) +- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054) +- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032) +- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046) +- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029) +- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031) +- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034) +- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037) +- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030) +- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025) +- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024) +- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009) + #### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1) +> 30 June 2022 + +- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021) - automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001) - 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998) - Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996) diff --git a/DOCKER_MORE_CLOSELY.md b/DOCKER_MORE_CLOSELY.md new file mode 100644 index 000000000..f2aae81c7 --- /dev/null +++ b/DOCKER_MORE_CLOSELY.md @@ -0,0 +1,44 @@ +# Docker More Closely + +## Apple M1 Platform + +***Attention:** For using Docker commands in Apple M1 environments!* + +### Enviroment Variable For Apple M1 Platform + +To set the Docker platform environment variable in your terminal tab, run: + +```bash +# set env variable for your shell +$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` + +### Docker Compose Override File For Apple M1 Platform + +For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: + +```bash +# in main folder + +# for development +$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up + +# for production +$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up +``` + +## Analysing Docker Builds + +To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! + +The `dive build` command is exactly the right one to fulfill what we are looking for. +We can use it just like the `docker build` command and get an analysis afterwards. + +So, in our main folder, we use it in the following way: + +```bash +# in main folder +$ dive build --target -t "gradido/:local-" / +``` + +For the specific applications, see our [publish.yml](.github/workflows/publish.yml). diff --git a/README.md b/README.md index 289a39109..3d086018e 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ After generating a new version you should commit the changes. This will be the C Note: The Changelog will be regenerated with all tags on release on the external builder tool, but will not be checked in there. The Changelog on the github release will therefore always be correct, on the repo it might be incorrect due to missing tags when executing the `yarn release` command. +## How the different .env work on deploy + +Each component (frontend, admin, backend and database) has its own `.env` file. When running in development with docker and nginx you usually do not have to care about the `.env`. The defaults are set by the respective config file, found in the `src/config/` folder of each component. But if you have a local `.env`, the defaults set in the config are overwritten by the `.env`. If you do not use docker, you need the `.env` in the frontend and admin interface because nginx is not running in order to find the backend. + +Each component has a `.env.dist` file. This file contains all environment variables used by the component and can be used as pattern. If you want to use a local `.env`, copy the `.env.dist` and adjust the variables accordingly. + +Each component has a `.env.template` file. These files are very important on deploy. + +There is one `.env.dist` in the `deployment/bare_metal/` folder. This `.env.dist` contains all variables used by the components, e.g. unites all `.env.dist` from the components. On deploy, we copy this `.env.dist` to `.env` and set all variables in this new file. The deploy script loads this variables and provides them by the `.env.templates` of each component, creating an `.env` for each component (see in `deployment/bare_metal/start.sh` the `envsubst`). + +To avoid forgetting to update an existing `.env` in the `deployment/bare_metal/` folder when deploying, we have an environment version variable inside the codebase of each component. You should update this version, when environment variables must be changed or added on deploy. The code checks, that the environement version provided by the `.env` is the one expected by the codebase. + + ## Troubleshooting | Problem | Issue | Solution | Description | diff --git a/admin/jest.config.js b/admin/jest.config.js index 9b9842bad..9233dd2e7 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -26,5 +26,5 @@ module.exports = { testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], - testEnvironment: 'jest-environment-jsdom-sixteen', + 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 50145d44a..2db889771 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.10.1", + "version": "1.13.1", "license": "Apache-2.0", "private": false, "scripts": { @@ -39,6 +39,7 @@ "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": "^2.1.7", "qrcanvas-vue": "2.1.1", "regenerator-runtime": "^0.13.9", @@ -70,7 +71,6 @@ "eslint-plugin-prettier": "3.3.1", "eslint-plugin-promise": "^5.1.1", "eslint-plugin-vue": "^7.20.0", - "jest-environment-jsdom-sixteen": "^2.0.0", "postcss": "^8.4.8", "postcss-html": "^1.3.0", "postcss-scss": "^4.0.3", diff --git a/admin/src/components/CommunityStatistic.spec.js b/admin/src/components/CommunityStatistic.spec.js new file mode 100644 index 000000000..dbcca5fed --- /dev/null +++ b/admin/src/components/CommunityStatistic.spec.js @@ -0,0 +1,39 @@ +import { mount } from '@vue/test-utils' +import CommunityStatistic from './CommunityStatistic' + +const localVue = global.localVue + +const mocks = { + $t: jest.fn((t) => t), + $n: jest.fn((n) => n), +} + +const propsData = { + value: { + totalUsers: '123', + activeUsers: '100', + deletedUsers: '5', + totalGradidoCreated: '2500', + totalGradidoDecayed: '200', + totalGradidoAvailable: '500', + totalGradidoUnbookedDecayed: '111', + }, +} + +describe('CommunityStatistic', () => { + let wrapper + + const Wrapper = () => { + return mount(CommunityStatistic, { localVue, mocks, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the Div Element ".community-statistic"', () => { + expect(wrapper.find('div.community-statistic').exists()).toBe(true) + }) + }) +}) diff --git a/admin/src/components/CommunityStatistic.vue b/admin/src/components/CommunityStatistic.vue new file mode 100644 index 000000000..c19f8deec --- /dev/null +++ b/admin/src/components/CommunityStatistic.vue @@ -0,0 +1,59 @@ + + diff --git a/admin/src/components/ContentFooter.spec.js b/admin/src/components/ContentFooter.spec.js new file mode 100644 index 000000000..b7b8d5aef --- /dev/null +++ b/admin/src/components/ContentFooter.spec.js @@ -0,0 +1,29 @@ +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'), + }, +} + +describe('ContentFooter', () => { + let wrapper + + const Wrapper = () => { + return mount(ContentFooter, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the div element ".content-footer"', () => { + expect(wrapper.find('div.content-footer').exists()).toBe(true) + }) + }) +}) diff --git a/admin/src/components/ContentFooter.vue b/admin/src/components/ContentFooter.vue index c10e53596..bab3f5d12 100644 --- a/admin/src/components/ContentFooter.vue +++ b/admin/src/components/ContentFooter.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js new file mode 100644 index 000000000..7504854d0 --- /dev/null +++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js @@ -0,0 +1,178 @@ +import { mount } from '@vue/test-utils' +import ContributionMessagesList from './ContributionMessagesList.vue' +import ContributionMessagesListItem from './ContributionMessagesListItem.vue' + +const localVue = global.localVue +let wrapper + +const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + $store: { + state: { + firstName: 'Peter', + lastName: 'Lustig', + }, + }, +} + +describe('ContributionMessagesList', () => { + const propsData = { + contributionId: 42, + state: 'PENDING', + messages: [ + { + id: 111, + message: 'Lorem ipsum?', + createdAt: '2022-08-29T12:23:27.000Z', + updatedAt: null, + type: 'DIALOG', + userFirstName: 'Peter', + userLastName: 'Lustig', + userId: 107, + __typename: 'ContributionMessage', + }, + { + 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', + }, + ], + } + + const ListWrapper = () => { + return mount(ContributionMessagesList, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = ListWrapper() + }) + + it('has two DIV .contribution-messages-list-item elements', () => { + expect(wrapper.findAll('div.contribution-messages-list-item').length).toBe(2) + }) + }) +}) + +describe('ContributionMessagesListItem', () => { + describe('if message author has moderator role', () => { + const propsData = { + 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', + }, + } + + const ItemWrapper = () => { + return mount(ContributionMessagesListItem, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeAll(() => { + wrapper = ItemWrapper() + }) + + it('has a DIV .is-moderator.text-left', () => { + expect(wrapper.find('div.is-moderator.text-left').exists()).toBe(true) + }) + + it('has the complete user name', () => { + expect(wrapper.find('div.is-moderator.text-left > span:nth-child(2)').text()).toBe( + 'Bibi Bloxberg', + ) + }) + + it('has the message creation date', () => { + expect(wrapper.find('div.is-moderator.text-left > span:nth-child(3)').text()).toMatch( + 'Mon Aug 29 2022 12:25:34 GMT+0000', + ) + }) + + it('has the moderator label', () => { + expect(wrapper.find('div.is-moderator.text-left > small:nth-child(4)').text()).toBe( + 'community.moderator', + ) + }) + + it('has the message', () => { + expect(wrapper.find('div.is-moderator.text-left > div:nth-child(5)').text()).toBe( + 'Asda sdad ad asdasd, das Ass das Das.', + ) + }) + }) + }) + + describe('if message author does not have moderator role', () => { + const propsData = { + 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, + }) + } + + describe('mount', () => { + beforeAll(() => { + wrapper = ModeratorItemWrapper() + }) + + it('has a DIV .is-not-moderator.text-right', () => { + expect(wrapper.find('div.is-not-moderator.text-right').exists()).toBe(true) + }) + + it('has the complete user name', () => { + expect(wrapper.find('div.is-not-moderator.text-right > span:nth-child(2)').text()).toBe( + 'Peter Lustig', + ) + }) + + it('has the message creation date', () => { + expect(wrapper.find('div.is-not-moderator.text-right > span:nth-child(3)').text()).toMatch( + 'Mon Aug 29 2022 12:23:27 GMT+0000', + ) + }) + + it('has the message', () => { + expect(wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)').text()).toBe( + 'Lorem ipsum?', + ) + }) + }) + }) +}) diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue new file mode 100644 index 000000000..6c2e555f2 --- /dev/null +++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue @@ -0,0 +1,56 @@ + + + + diff --git a/frontend/src/components/Contributions/ContributionForm.spec.js b/frontend/src/components/Contributions/ContributionForm.spec.js new file mode 100644 index 000000000..8f35948f9 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionForm.spec.js @@ -0,0 +1,428 @@ +import { mount } from '@vue/test-utils' +import ContributionForm from './ContributionForm.vue' + +const localVue = global.localVue + +describe('ContributionForm', () => { + let wrapper + + const propsData = { + value: { + id: null, + date: '', + memo: '', + amount: '', + }, + } + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + $store: { + state: { + creation: ['1000', '1000', '1000'], + }, + }, + $i18n: { + locale: 'en', + }, + } + + const Wrapper = () => { + return mount(ContributionForm, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV .contribution-form', () => { + expect(wrapper.find('div.contribution-form').exists()).toBe(true) + }) + + describe('empty form data', () => { + describe('button', () => { + it('has submit disabled', () => { + expect(wrapper.find('button[data-test="button-submit"]').attributes('disabled')).toBe( + 'disabled', + ) + }) + }) + }) + + describe('dates', () => { + beforeEach(async () => { + await wrapper.setData({ + form: { + id: null, + date: '', + memo: '', + amount: '', + }, + }) + }) + + describe('actual date', () => { + describe('same month', () => { + beforeEach(async () => { + const now = new Date().toISOString() + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + }) + + describe('isThisMonth', () => { + it('has true', () => { + expect(wrapper.vm.isThisMonth).toBe(true) + }) + }) + }) + + describe('month before', () => { + beforeEach(async () => { + await wrapper + .findComponent({ name: 'BFormDatepicker' }) + .vm.$emit('input', wrapper.vm.minimalDate) + }) + + describe('isThisMonth', () => { + it('has false', () => { + expect(wrapper.vm.isThisMonth).toBe(false) + }) + }) + }) + }) + + describe('date in middle of year', () => { + describe('same month', () => { + beforeEach(async () => { + // jest.useFakeTimers('modern') + // jest.setSystemTime(new Date('2020-07-06')) + // await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper.setData({ + maximalDate: new Date(2020, 6, 6), + form: { date: new Date(2020, 6, 6) }, + }) + }) + + describe('minimalDate', () => { + it('has "2020-06-01T00:00:00.000Z"', () => { + expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') + }) + }) + + describe('isThisMonth', () => { + it('has true', () => { + expect(wrapper.vm.isThisMonth).toBe(true) + }) + }) + }) + + describe('month before', () => { + beforeEach(async () => { + // jest.useFakeTimers('modern') + // jest.setSystemTime(new Date('2020-07-06')) + // console.log('middle of year date – now:', wrapper.vm.minimalDate) + // await wrapper + // .findComponent({ name: 'BFormDatepicker' }) + // .vm.$emit('input', wrapper.vm.minimalDate) + await wrapper.setData({ + maximalDate: new Date(2020, 6, 6), + form: { date: new Date(2020, 5, 6) }, + }) + }) + + describe('minimalDate', () => { + it('has "2020-06-01T00:00:00.000Z"', () => { + expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z') + }) + }) + + describe('isThisMonth', () => { + it('has false', () => { + expect(wrapper.vm.isThisMonth).toBe(false) + }) + }) + }) + }) + + describe('date in january', () => { + describe('same month', () => { + beforeEach(async () => { + await wrapper.setData({ + maximalDate: new Date(2020, 0, 6), + form: { date: new Date(2020, 0, 6) }, + }) + }) + + describe('minimalDate', () => { + it('has "2019-12-01T00:00:00.000Z"', () => { + expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') + }) + }) + + describe('isThisMonth', () => { + it('has true', () => { + expect(wrapper.vm.isThisMonth).toBe(true) + }) + }) + }) + + describe('month before', () => { + beforeEach(async () => { + // jest.useFakeTimers('modern') + // jest.setSystemTime(new Date('2020-07-06')) + // console.log('middle of year date – now:', wrapper.vm.minimalDate) + // await wrapper + // .findComponent({ name: 'BFormDatepicker' }) + // .vm.$emit('input', wrapper.vm.minimalDate) + await wrapper.setData({ + maximalDate: new Date(2020, 0, 6), + form: { date: new Date(2019, 11, 6) }, + }) + }) + + describe('minimalDate', () => { + it('has "2019-12-01T00:00:00.000Z"', () => { + expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z') + }) + }) + + describe('isThisMonth', () => { + it('has false', () => { + expect(wrapper.vm.isThisMonth).toBe(false) + }) + }) + }) + }) + }) + + describe('set contrubtion', () => { + describe('fill in form data with "id === null"', () => { + const now = new Date().toISOString() + + beforeEach(async () => { + await wrapper.setData({ + form: { + id: null, + date: '', + memo: '', + amount: '', + }, + }) + }) + + describe('invalid form data', () => { + beforeEach(async () => { + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper.find('#contribution-amount').find('input').setValue('200') + }) + + describe('memo lenght < 5, others are valid', () => { + beforeEach(async () => { + await wrapper.find('#contribution-memo').find('textarea').setValue('1234') + }) + + describe('button', () => { + describe('submit', () => { + it('has disabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBe('disabled') + }) + }) + }) + }) + + describe('memo lenght > 255, others are valid', () => { + beforeEach(async () => { + await wrapper + .find('#contribution-memo') + .find('textarea') + .setValue( + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' + + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' + + '01234567890123456789012345678901234567890123456789012345', + ) + await wrapper.vm.$nextTick() + }) + + describe('button', () => { + describe('submit', () => { + it('has disabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBe('disabled') + }) + }) + }) + }) + }) + + describe('valid form data', () => { + beforeEach(async () => { + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper + .find('#contribution-memo') + .find('textarea') + .setValue('Mein Beitrag zur Gemeinschaft für diesen Monat ...') + await wrapper.find('#contribution-amount').find('input').setValue('200') + }) + + describe('button', () => { + describe('submit', () => { + it('has enabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBeFalsy() + }) + + it('has label "contribution.submit"', () => { + expect(wrapper.find('button[data-test="button-submit"]').text()).toContain( + 'contribution.submit', + ) + }) + }) + }) + + describe('on trigger submit', () => { + beforeEach(async () => { + await wrapper.find('form').trigger('submit') + }) + + it('emits "set-contribution"', () => { + expect(wrapper.emitted('set-contribution')).toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + { + id: null, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + ]), + ]), + ) + }) + }) + }) + }) + }) + + describe('update contrubtion', () => { + describe('fill in form data with set "id"', () => { + const now = new Date().toISOString() + + beforeEach(async () => { + await wrapper.setData({ + form: { + id: 2, + date: now, + memo: 'Mein kommerzieller Beitrag für diesen Monat ...', + amount: '100', + }, + }) + }) + + describe('invalid form data', () => { + beforeEach(async () => { + // skip this precondition as long as the datepicker is disabled in the component + // await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper.find('#contribution-amount').find('input').setValue('200') + }) + + describe('memo lenght < 5, others are valid', () => { + beforeEach(async () => { + await wrapper.find('#contribution-memo').find('textarea').setValue('1234') + }) + + describe('button', () => { + describe('submit', () => { + it('has disabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBe('disabled') + }) + }) + }) + }) + + describe('memo lenght > 255, others are valid', () => { + beforeEach(async () => { + await wrapper + .find('#contribution-memo') + .find('textarea') + .setValue( + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' + + '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' + + '01234567890123456789012345678901234567890123456789012345', + ) + await wrapper.vm.$nextTick() + }) + + describe('button', () => { + describe('submit', () => { + it('has disabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBe('disabled') + }) + }) + }) + }) + }) + + describe('valid form data', () => { + beforeEach(async () => { + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper + .find('#contribution-memo') + .find('textarea') + .setValue('Mein Beitrag zur Gemeinschaft für diesen Monat ...') + await wrapper.find('#contribution-amount').find('input').setValue('200') + }) + + describe('button', () => { + describe('submit', () => { + it('has enabled', () => { + expect( + wrapper.find('button[data-test="button-submit"]').attributes('disabled'), + ).toBeFalsy() + }) + + it('has label "form.change"', () => { + expect(wrapper.find('button[data-test="button-submit"]').text()).toContain( + 'form.change', + ) + }) + }) + }) + + describe('on trigger submit', () => { + beforeEach(async () => { + await wrapper.find('form').trigger('submit') + }) + + it('emits "update-contribution"', () => { + expect(wrapper.emitted('update-contribution')).toEqual( + expect.arrayContaining([ + expect.arrayContaining([ + { + id: 2, + date: now, + memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', + amount: '200', + }, + ]), + ]), + ) + }) + }) + }) + }) + }) + }) +}) diff --git a/frontend/src/components/Contributions/ContributionForm.vue b/frontend/src/components/Contributions/ContributionForm.vue new file mode 100644 index 000000000..efe80f494 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionForm.vue @@ -0,0 +1,174 @@ + + + diff --git a/frontend/src/components/Contributions/ContributionList.spec.js b/frontend/src/components/Contributions/ContributionList.spec.js new file mode 100644 index 000000000..a1dfc934d --- /dev/null +++ b/frontend/src/components/Contributions/ContributionList.spec.js @@ -0,0 +1,120 @@ +import { mount } from '@vue/test-utils' +import ContributionList from './ContributionList.vue' + +const localVue = global.localVue + +describe('ContributionList', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + } + + const propsData = { + contributionCount: 3, + showPagination: true, + pageSize: 25, + items: [ + { + id: 0, + date: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + }, + { + id: 1, + date: '06/22/2022', + memo: 'Ich habe 30 Stunden Frau Müller beim EInkaufen und im Haushalt geholfen.', + amount: '600', + }, + { + id: 2, + date: '05/04/2022', + memo: + 'Ich habe 50 Stunden den Nachbarkindern bei ihren Hausaufgaben geholfen und Nachhilfeunterricht gegeben.', + amount: '1000', + }, + ], + } + + const Wrapper = () => { + return mount(ContributionList, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV .contribution-list', () => { + expect(wrapper.find('div.contribution-list').exists()).toBe(true) + }) + + describe('pagination', () => { + describe('list count smaller than page size', () => { + it('has no pagination buttons', () => { + expect(wrapper.find('ul.pagination').exists()).toBe(false) + }) + }) + + describe('list count greater than page size', () => { + beforeEach(() => { + wrapper.setProps({ contributionCount: 33 }) + }) + + it('has pagination buttons', () => { + expect(wrapper.find('ul.pagination').exists()).toBe(true) + }) + }) + + describe('switch page', () => { + const scrollToMock = jest.fn() + window.scrollTo = scrollToMock + + beforeEach(async () => { + await wrapper.setProps({ contributionCount: 33 }) + wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2) + }) + + it('emits update contribution list', () => { + expect(wrapper.emitted('update-list-contributions')).toEqual([ + [{ currentPage: 2, pageSize: 25 }], + ]) + }) + + it('scrolls to top', () => { + expect(scrollToMock).toBeCalledWith(0, 0) + }) + }) + }) + + describe('update contribution', () => { + beforeEach(() => { + wrapper + .findComponent({ name: 'ContributionListItem' }) + .vm.$emit('update-contribution-form', 'item') + }) + + it('emits update contribution form', () => { + expect(wrapper.emitted('update-contribution-form')).toEqual([['item']]) + }) + }) + + describe('delete contribution', () => { + beforeEach(() => { + wrapper + .findComponent({ name: 'ContributionListItem' }) + .vm.$emit('delete-contribution', { id: 2 }) + }) + + it('emits delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]]) + }) + }) + }) +}) diff --git a/frontend/src/components/Contributions/ContributionList.vue b/frontend/src/components/Contributions/ContributionList.vue new file mode 100644 index 000000000..ca4e7a9a0 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionList.vue @@ -0,0 +1,89 @@ + + diff --git a/frontend/src/components/Contributions/ContributionListItem.spec.js b/frontend/src/components/Contributions/ContributionListItem.spec.js new file mode 100644 index 000000000..0b0519dda --- /dev/null +++ b/frontend/src/components/Contributions/ContributionListItem.spec.js @@ -0,0 +1,137 @@ +import { mount } from '@vue/test-utils' +import ContributionListItem from './ContributionListItem.vue' + +const localVue = global.localVue + +describe('ContributionListItem', () => { + let wrapper + + const mocks = { + $t: jest.fn((t) => t), + $d: jest.fn((d) => d), + } + + const propsData = { + contributionId: 42, + state: 'PENDING', + messagesCount: 2, + id: 1, + createdAt: '26/07/2022', + contributionDate: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + } + + const Wrapper = () => { + return mount(ContributionListItem, { + localVue, + mocks, + propsData, + }) + } + + describe('mount', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper = Wrapper() + }) + + it('has a DIV .contribution-list-item', () => { + expect(wrapper.find('div.contribution-list-item').exists()).toBe(true) + }) + + describe('contribution icon', () => { + it('is bell-fill by default', () => { + expect(wrapper.vm.icon).toBe('bell-fill') + }) + + it('is x-circle when deletedAt is present', async () => { + await wrapper.setProps({ deletedAt: new Date().toISOString() }) + expect(wrapper.vm.icon).toBe('x-circle') + }) + + it('is check when confirmedAt is present', async () => { + await wrapper.setProps({ confirmedAt: new Date().toISOString() }) + expect(wrapper.vm.icon).toBe('check') + }) + }) + + describe('contribution variant', () => { + it('is primary by default', () => { + expect(wrapper.vm.variant).toBe('primary') + }) + + it('is danger when deletedAt is present', async () => { + await wrapper.setProps({ deletedAt: new Date().toISOString() }) + expect(wrapper.vm.variant).toBe('danger') + }) + + it('is success at when confirmedAt is present', async () => { + await wrapper.setProps({ confirmedAt: new Date().toISOString() }) + expect(wrapper.vm.variant).toBe('success') + }) + + it('is warning at when state is IN_PROGRESS', async () => { + await wrapper.setProps({ state: 'IN_PROGRESS' }) + expect(wrapper.vm.variant).toBe('warning') + }) + }) + + describe('date', () => { + it('is equal to createdAt', () => { + expect(wrapper.vm.date).toBe(wrapper.vm.createdAt) + }) + }) + + describe('delete contribution', () => { + let spy + + describe('edit contribution', () => { + beforeEach(() => { + wrapper.findAll('div.pointer').at(0).trigger('click') + }) + + it('emits update contribution form', () => { + expect(wrapper.emitted('update-contribution-form')).toEqual([ + [ + { + id: 1, + contributionDate: '07/06/2022', + memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.', + amount: '200', + }, + ], + ]) + }) + }) + + describe('confirm deletion', () => { + beforeEach(() => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(true)) + wrapper.findAll('div.pointer').at(1).trigger('click') + }) + + it('opens the modal', () => { + expect(spy).toBeCalledWith('contribution.delete') + }) + + it('emits delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 1 }]]) + }) + }) + + describe('cancel deletion', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(false)) + await wrapper.findAll('div.pointer').at(2).trigger('click') + }) + + it('does not emit delete contribution', () => { + expect(wrapper.emitted('delete-contribution')).toBeFalsy() + }) + }) + }) + }) +}) diff --git a/frontend/src/components/Contributions/ContributionListItem.vue b/frontend/src/components/Contributions/ContributionListItem.vue new file mode 100644 index 000000000..683d234ba --- /dev/null +++ b/frontend/src/components/Contributions/ContributionListItem.vue @@ -0,0 +1,204 @@ + + diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js index a28c2d185..91207901c 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js @@ -46,5 +46,17 @@ describe('GddSend confirm', () => { expect(wrapper.findAll('div.confirm-box-link').at(0).exists()).toBeTruthy() }) }) + + describe('has totalBalance under 0', () => { + beforeEach(async () => { + await wrapper.setProps({ + balance: 0, + }) + }) + + it('has send button disabled', () => { + expect(wrapper.find('.send-button').attributes('disabled')).toBe('disabled') + }) + }) }) }) diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.vue b/frontend/src/components/GddSend/TransactionConfirmationLink.vue index c23ed35d2..2620753ee 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.vue @@ -42,7 +42,12 @@ {{ $t('back') }} - + {{ $t('form.generate_now') }} diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index 62361c4d0..69c6b6b45 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -46,7 +46,7 @@ @@ -81,7 +81,11 @@ v-slot="{ errors, valid }" > - +
{{ $t('GDD') }}
@@ -115,7 +119,7 @@ v-slot="{ errors }" > - + @@ -237,4 +241,7 @@ span.errors { #input-3:focus { font-weight: bold; } +.border-radius { + border-radius: 10px; +} diff --git a/frontend/src/components/GddSend/TransactionResultLink.vue b/frontend/src/components/GddSend/TransactionResultLink.vue index 04445acfe..2e1e68ac0 100644 --- a/frontend/src/components/GddSend/TransactionResultLink.vue +++ b/frontend/src/components/GddSend/TransactionResultLink.vue @@ -3,7 +3,13 @@
{{ $t('gdd_per_link.created') }}
- +
@@ -27,10 +33,10 @@ export default { FigureQrCode, }, props: { - link: { - type: String, - required: true, - }, + link: { type: String, required: true }, + amount: { type: String, required: true }, + memo: { type: String, required: true }, + validUntil: { type: String, required: true }, }, data() { return { diff --git a/frontend/src/components/GddTransactionList.vue b/frontend/src/components/GddTransactionList.vue index 5becfa39e..a74be5187 100644 --- a/frontend/src/components/GddTransactionList.vue +++ b/frontend/src/components/GddTransactionList.vue @@ -69,6 +69,7 @@ :per-page="pageSize" :total-rows="transactionCount" align="center" + :hide-ellipsis="true" >
diff --git a/frontend/src/components/GdtTransactionList.vue b/frontend/src/components/GdtTransactionList.vue index 4934f9fce..f915cd881 100644 --- a/frontend/src/components/GdtTransactionList.vue +++ b/frontend/src/components/GdtTransactionList.vue @@ -36,6 +36,7 @@ :per-page="pageSize" :total-rows="transactionGdtCount" align="center" + :hide-ellipsis="true" >
diff --git a/frontend/src/components/Inputs/InputPasswordConfirmation.vue b/frontend/src/components/Inputs/InputPasswordConfirmation.vue index 8154984ef..56d58d9ad 100644 --- a/frontend/src/components/Inputs/InputPasswordConfirmation.vue +++ b/frontend/src/components/Inputs/InputPasswordConfirmation.vue @@ -12,6 +12,7 @@ atLeastOneSpecialCharater: true, noWhitespaceCharacters: true, }" + id="new-password-input-field" :label="register ? $t('form.password') : $t('form.password_new')" :showAllErrors="true" :immediate="true" @@ -28,6 +29,7 @@ required: true, samePassword: value.password, }" + id="repeat-new-password-input-field" :label="register ? $t('form.passwordRepeat') : $t('form.password_new_repeat')" :immediate="true" :name="createId(register ? $t('form.passwordRepeat') : $t('form.password_new_repeat'))" diff --git a/frontend/src/components/LanguageSwitch.spec.js b/frontend/src/components/LanguageSwitch.spec.js index cf7c4a35e..7f37c535a 100644 --- a/frontend/src/components/LanguageSwitch.spec.js +++ b/frontend/src/components/LanguageSwitch.spec.js @@ -45,7 +45,7 @@ describe('LanguageSwitch', () => { expect(wrapper.find('div.language-switch').exists()).toBeTruthy() }) - describe('with locales en and de', () => { + describe('with locales en, de, es, fr, and nl', () => { describe('empty store', () => { describe('navigator language is "en-US"', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') @@ -69,11 +69,44 @@ describe('LanguageSwitch', () => { }) }) - describe('navigator language is "es-ES" (not supported)', () => { + describe('navigator language is "es-ES"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows Español as language ', async () => { + languageGetter.mockReturnValue('es-ES') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es') + }) + }) + + describe('navigator language is "fr-FR"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows French as language ', async () => { + languageGetter.mockReturnValue('fr-FR') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Français - fr') + }) + }) + + describe('navigator language is "nl-NL"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows Nederlands as language ', async () => { + languageGetter.mockReturnValue('nl-NL') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Nederlands - nl') + }) + }) + + describe('navigator language is "it-IT" (not supported)', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') it('shows English as language ', async () => { - languageGetter.mockReturnValue('es-ES') + languageGetter.mockReturnValue('it-IT') wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en') @@ -101,9 +134,36 @@ describe('LanguageSwitch', () => { }) }) + describe('language "es" in store', () => { + it('shows Español as language', async () => { + wrapper.vm.$store.state.language = 'es' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es') + }) + }) + + describe('language "fr" in store', () => { + it('shows French as language', async () => { + wrapper.vm.$store.state.language = 'fr' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Français - fr') + }) + }) + + describe('language "nl" in store', () => { + it('shows Nederlands as language', async () => { + wrapper.vm.$store.state.language = 'nl' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.find('button.dropdown-toggle').text()).toBe('Nederlands - nl') + }) + }) + describe('dropdown menu', () => { - it('has English and German as languages to choose', () => { - expect(wrapper.findAll('li')).toHaveLength(2) + it('has five languages to choose from', () => { + expect(wrapper.findAll('li')).toHaveLength(5) }) it('has English as first language to choose', () => { @@ -113,6 +173,18 @@ describe('LanguageSwitch', () => { it('has German as second language to choose', () => { expect(wrapper.findAll('li').at(1).text()).toBe('Deutsch') }) + + it('has Español as third language to choose', () => { + expect(wrapper.findAll('li').at(2).text()).toBe('Español') + }) + + it('has French as fourth language to choose', () => { + expect(wrapper.findAll('li').at(3).text()).toBe('Français') + }) + + it('has Nederlands as fith language to choose', () => { + expect(wrapper.findAll('li').at(4).text()).toBe('Nederlands') + }) }) }) @@ -138,6 +210,39 @@ describe('LanguageSwitch', () => { }), ) }) + + it("with locale 'es'", () => { + wrapper.findAll('li').at(2).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'es', + }, + }), + ) + }) + + it("with locale 'fr'", () => { + wrapper.findAll('li').at(3).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'fr', + }, + }), + ) + }) + + it("with locale 'nl'", () => { + wrapper.findAll('li').at(4).find('a').trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ + variables: { + locale: 'nl', + }, + }), + ) + }) }) }) }) diff --git a/frontend/src/components/LanguageSwitch2.spec.js b/frontend/src/components/LanguageSwitch2.spec.js index 600e2513e..0d2b485ec 100644 --- a/frontend/src/components/LanguageSwitch2.spec.js +++ b/frontend/src/components/LanguageSwitch2.spec.js @@ -46,10 +46,11 @@ describe('LanguageSwitch', () => { expect(wrapper.find('div.language-switch').exists()).toBe(true) }) - describe('with locales en and de', () => { + describe('with locales en, de, es, fr, and nl', () => { describe('empty store', () => { describe('navigator language is "en-US"', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows English as default navigator langauge', async () => { languageGetter.mockReturnValue('en-US') wrapper.vm.setCurrentLanguage() @@ -57,8 +58,10 @@ describe('LanguageSwitch', () => { expect(wrapper.findAll('span.locales').at(0).text()).toBe('English') }) }) + describe('navigator language is "de-DE"', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows Deutsch as language ', async () => { languageGetter.mockReturnValue('de-DE') wrapper.vm.setCurrentLanguage() @@ -66,17 +69,54 @@ describe('LanguageSwitch', () => { expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') }) }) - describe('navigator language is "es-ES" (not supported)', () => { + + describe('navigator language is "es-ES"', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') - it('shows English as language ', async () => { + + it('shows Español as language ', async () => { languageGetter.mockReturnValue('es-ES') wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español') + }) + }) + + describe('navigator language is "fr-FR"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows French as language ', async () => { + languageGetter.mockReturnValue('fr-FR') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Français') + }) + }) + + describe('navigator language is "nl-NL"', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows Nederlands as language ', async () => { + languageGetter.mockReturnValue('nl-NL') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Nederlands') + }) + }) + + describe('navigator language is "it-IT" (not supported)', () => { + const languageGetter = jest.spyOn(navigator, 'language', 'get') + + it('shows English as language ', async () => { + languageGetter.mockReturnValue('it-IT') + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() expect(wrapper.findAll('span.locales').at(0).text()).toBe('English') }) }) + describe('no navigator langauge', () => { const languageGetter = jest.spyOn(navigator, 'language', 'get') + it('shows English as language ', async () => { languageGetter.mockReturnValue(null) wrapper.vm.setCurrentLanguage() @@ -85,24 +125,73 @@ describe('LanguageSwitch', () => { }) }) }) + describe('language "de" in store', () => { it('shows Deutsch as language', async () => { wrapper.vm.$store.state.language = 'de' wrapper.vm.setCurrentLanguage() await wrapper.vm.$nextTick() - expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') + expect(wrapper.findAll('span.locales').at(1).text()).toBe('English') }) }) - describe('language menu', () => { - it('has English and German as languages to choose', () => { - expect(wrapper.findAll('span.locales')).toHaveLength(2) + + describe('language "es" in store', () => { + it('shows Español as language', async () => { + wrapper.vm.$store.state.language = 'es' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Deutsch') }) + }) + + describe('language "fr" in store', () => { + it('shows French as language', async () => { + wrapper.vm.$store.state.language = 'fr' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Español') + }) + }) + + describe('language "nl" in store', () => { + it('shows Nederlands as language', async () => { + wrapper.vm.$store.state.language = 'nl' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Français') + }) + }) + + describe('language menu', () => { + beforeAll(async () => { + wrapper.vm.$store.state.language = 'en' + wrapper.vm.setCurrentLanguage() + await wrapper.vm.$nextTick() + }) + + it('has five languages to choose from', () => { + expect(wrapper.findAll('span.locales')).toHaveLength(5) + }) + it('has English as first language to choose', () => { expect(wrapper.findAll('span.locales').at(0).text()).toBe('English') }) - it('has German as second language to choose', () => { + + it('has Deutsch as second language to choose', () => { expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch') }) + + it('has Español as third language to choose', () => { + expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español') + }) + + it('has Français as fourth language to choose', () => { + expect(wrapper.findAll('span.locales').at(3).text()).toBe('Français') + }) + + it('has Nederlands as fifth language to choose', () => { + expect(wrapper.findAll('span.locales').at(4).text()).toBe('Nederlands') + }) }) }) @@ -110,24 +199,30 @@ describe('LanguageSwitch', () => { it("with locale 'de'", () => { wrapper.findAll('span.locales').at(1).trigger('click') expect(updateUserInfosMutationMock).toBeCalledWith( - expect.objectContaining({ - variables: { - locale: 'de', - }, - }), + expect.objectContaining({ variables: { locale: 'de' } }), ) }) - // it("with locale 'en'", () => { - // wrapper.findAll('span.locales').at(0).trigger('click') - // expect(updateUserInfosMutationMock).toBeCalledWith( - // expect.objectContaining({ - // variables: { - // locale: 'en', - // }, - // }), - // ) - // }) + it("with locale 'es'", () => { + wrapper.findAll('span.locales').at(2).trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ variables: { locale: 'es' } }), + ) + }) + + it("with locale 'fr'", () => { + wrapper.findAll('span.locales').at(3).trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ variables: { locale: 'fr' } }), + ) + }) + + it("with locale 'nl'", () => { + wrapper.findAll('span.locales').at(4).trigger('click') + expect(updateUserInfosMutationMock).toBeCalledWith( + expect.objectContaining({ variables: { locale: 'nl' } }), + ) + }) }) }) }) diff --git a/frontend/src/components/LanguageSwitch2.vue b/frontend/src/components/LanguageSwitch2.vue index b78154e0a..d398d2fe0 100644 --- a/frontend/src/components/LanguageSwitch2.vue +++ b/frontend/src/components/LanguageSwitch2.vue @@ -1,15 +1,41 @@ @@ -68,6 +94,17 @@ export default { this.currentLanguage = object }, }, + computed: { + indexOfSelectedLocale() { + return this.locales.findIndex((element) => element.code === this.$store.state.language) + }, + indexOfSecondLastLocale() { + return this.locales.length - 2 + }, + indexOfLastLocale() { + return this.locales.length - 1 + }, + }, created() { this.setCurrentLanguage() }, diff --git a/frontend/src/components/LanguageSwitchSelect.vue b/frontend/src/components/LanguageSwitchSelect.vue index 545cef4e9..109c020ff 100644 --- a/frontend/src/components/LanguageSwitchSelect.vue +++ b/frontend/src/components/LanguageSwitchSelect.vue @@ -16,6 +16,9 @@ export default { options: [ { value: 'de', text: this.$t('settings.language.de') }, { value: 'en', text: this.$t('settings.language.en') }, + { value: 'es', text: this.$t('settings.language.es') }, + { value: 'fr', text: this.$t('settings.language.fr') }, + { value: 'nl', text: this.$t('settings.language.nl') }, ], } }, diff --git a/frontend/src/components/LinkInformations/RedeemLoggedOut.vue b/frontend/src/components/LinkInformations/RedeemLoggedOut.vue index 982bfdf08..6673b3c5b 100644 --- a/frontend/src/components/LinkInformations/RedeemLoggedOut.vue +++ b/frontend/src/components/LinkInformations/RedeemLoggedOut.vue @@ -25,9 +25,11 @@ diff --git a/frontend/src/components/LinkInformations/RedeemValid.vue b/frontend/src/components/LinkInformations/RedeemValid.vue index 353fefaf8..c468a396a 100644 --- a/frontend/src/components/LinkInformations/RedeemValid.vue +++ b/frontend/src/components/LinkInformations/RedeemValid.vue @@ -3,7 +3,7 @@
- + {{ $t('gdd_per_link.redeem') }}
diff --git a/frontend/src/components/Menu/Navbar.spec.js b/frontend/src/components/Menu/Navbar.spec.js index ebf9abba0..1e08ad9dc 100644 --- a/frontend/src/components/Menu/Navbar.spec.js +++ b/frontend/src/components/Menu/Navbar.spec.js @@ -17,7 +17,7 @@ const mocks = { $t: jest.fn((t) => t), $store: { state: { - hasElopage: false, + hasElopage: true, isAdmin: true, }, }, @@ -39,49 +39,85 @@ describe('Navbar', () => { expect(wrapper.find('div.component-navbar').exists()).toBeTruthy() }) - describe('navigation Navbar', () => { + describe('navigation Navbar (general elements)', () => { it('has .navbar-brand in the navbar', () => { expect(wrapper.find('.navbar-brand').exists()).toBeTruthy() }) + it('has b-navbar-toggle in the navbar', () => { expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy() }) - it('has ten b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(10) + + it('has thirteen b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(13) }) - it('has first nav-item "amount GDD" in navbar', () => { + it('has nav-item "amount GDD" in navbar', () => { expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('1234 GDD') }) - it('has first nav-item "navigation.overview" in navbar', () => { + it('has nav-item "navigation.overview" in navbar', () => { expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.overview') }) - it('has first nav-item "navigation.send" in navbar', () => { + + it('has nav-item "navigation.send" in navbar', () => { expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.send') }) - it('has first nav-item "navigation.transactions" in navbar', () => { + + it('has nav-item "navigation.transactions" in navbar', () => { expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions') }) - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.profile') + + it('has nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('gdt.gdt') }) + it('has nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.community') + }) + + it('has nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.profile') + }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.info') + }) + }) + + describe('navigation Navbar (user has an elopage account)', () => { it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(7).text()).toContain('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe( + expect(wrapper.findAll('.nav-item').at(10).text()).toContain('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(10).find('a').attributes('href')).toBe( 'https://elopage.com', ) }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(12).text()).toEqual('navigation.logout') + }) + }) + + describe('navigation Navbar (user has no elopage account)', () => { + beforeAll(() => { + mocks.$store.state.hasElopage = false + wrapper = Wrapper() + }) + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.logout') }) }) }) + describe('check watch visible true', () => { beforeEach(async () => { await wrapper.setProps({ visible: true }) diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index 2f26f381e..8727dbd2f 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -14,7 +14,12 @@
{{ pending ? $t('em-dash') : balance | amount }} {{ $t('GDD') }}
- + {{ $store.state.firstName }} {{ $store.state.lastName }} {{ $store.state.email }} @@ -52,17 +57,26 @@ {{ $t('navigation.transactions') }} + + + {{ $t('gdt.gdt') }} + + + + {{ $t('navigation.community') }} + {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
- + {{ $t('navigation.members_area') }} - - {{ $t('math.exclaim') }} - diff --git a/frontend/src/components/Menu/Sidebar.spec.js b/frontend/src/components/Menu/Sidebar.spec.js index fec7945c8..612649762 100644 --- a/frontend/src/components/Menu/Sidebar.spec.js +++ b/frontend/src/components/Menu/Sidebar.spec.js @@ -27,38 +27,86 @@ describe('Sidebar', () => { beforeEach(() => { wrapper = Wrapper() }) + it('renders the component', () => { expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy() }) describe('navigation Navbar', () => { - it('has seven b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(7) + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) }) - it('has first nav-item "navigation.overview" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + describe('navigation Navbar (general elements)', () => { + it('has nav-item "navigation.overview" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + }) + + it('has nav-item "navigation.send" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + }) + + it('has nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('gdt.gdt') + }) + + it('has nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.community') + }) + + it('has nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.profile') + }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.info') + }) }) - it('has first nav-item "navigation.send" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + describe('navigation Navbar (user has an elopage account)', () => { + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) + }) + + it('has a link to the members area', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe('#') + }) + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') + }) }) - it('has first nav-item "navigation.transactions" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.profile') + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') }) - it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('#') + }) + + describe('navigation Navbar (user has no elopage account)', () => { + beforeAll(() => { + mocks.$store.state.hasElopage = false + wrapper = Wrapper() }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.admin_area') + + it('has nine b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(9) }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.logout') + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.logout') }) }) }) diff --git a/frontend/src/components/Menu/Sidebar.vue b/frontend/src/components/Menu/Sidebar.vue index 028b7aca6..f6abb6fbc 100644 --- a/frontend/src/components/Menu/Sidebar.vue +++ b/frontend/src/components/Menu/Sidebar.vue @@ -16,25 +16,39 @@ {{ $t('navigation.transactions') }} - + + + {{ $t('gdt.gdt') }} + + + + {{ $t('navigation.community') }} + + {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
- + {{ $t('navigation.members_area') }} - - {{ $t('math.exclaim') }} - {{ $t('navigation.admin_area') }} - + {{ $t('navigation.logout') }} diff --git a/frontend/src/components/SessionLogoutTimeout.spec.js b/frontend/src/components/SessionLogoutTimeout.spec.js index 0f5d21d36..bd6911d13 100644 --- a/frontend/src/components/SessionLogoutTimeout.spec.js +++ b/frontend/src/components/SessionLogoutTimeout.spec.js @@ -62,12 +62,16 @@ describe('SessionLogoutTimeout', () => { }) }) - describe('token is expired', () => { + describe('token is expired for several seconds', () => { beforeEach(() => { mocks.$store.state.tokenTime = setTokenTime(-60) wrapper = Wrapper() }) + it('has value for remaining seconds equal 0', () => { + expect(wrapper.tokenExpiresInSeconds === 0) + }) + it('emits logout', () => { expect(wrapper.emitted('logout')).toBeTruthy() }) diff --git a/frontend/src/components/SessionLogoutTimeout.vue b/frontend/src/components/SessionLogoutTimeout.vue index 1e5a27998..1ebff752a 100644 --- a/frontend/src/components/SessionLogoutTimeout.vue +++ b/frontend/src/components/SessionLogoutTimeout.vue @@ -65,7 +65,7 @@ export default { this.$timer.restart('tokenExpires') this.$bvModal.show('modalSessionTimeOut') } - if (this.tokenExpiresInSeconds <= 0) { + if (this.tokenExpiresInSeconds === 0) { this.$timer.stop('tokenExpires') this.$emit('logout') } @@ -90,7 +90,10 @@ export default { }, computed: { tokenExpiresInSeconds() { - return Math.floor((new Date(this.$store.state.tokenTime * 1000).getTime() - this.now) / 1000) + const remainingSecs = Math.floor( + (new Date(this.$store.state.tokenTime * 1000).getTime() - this.now) / 1000, + ) + return remainingSecs <= 0 ? 0 : remainingSecs }, }, beforeDestroy() { diff --git a/frontend/src/components/TransactionLinks/TransactionLink.spec.js b/frontend/src/components/TransactionLinks/TransactionLink.spec.js index 13aaea900..798223f60 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.spec.js +++ b/frontend/src/components/TransactionLinks/TransactionLink.spec.js @@ -9,15 +9,16 @@ const mockAPIcall = jest.fn() const navigatorClipboardMock = jest.fn() const mocks = { - $i18n: { - locale: 'en', - }, $t: jest.fn((t) => t), $d: jest.fn((d) => d), - $tc: jest.fn((tc) => tc), $apollo: { mutate: mockAPIcall, }, + $store: { + state: { + firstName: 'Testy', + }, + }, } const propsData = { @@ -77,7 +78,7 @@ describe('TransactionLink', () => { navigator.clipboard = navigatorClipboard }) - describe('copy with success', () => { + describe('copy link with success', () => { beforeEach(async () => { navigatorClipboardMock.mockResolvedValue() await wrapper.find('.test-copy-link .dropdown-item').trigger('click') @@ -93,7 +94,27 @@ describe('TransactionLink', () => { }) }) - describe('copy with error', () => { + describe('copy link and text with success', () => { + beforeEach(async () => { + navigatorClipboardMock.mockResolvedValue() + await wrapper.find('.test-copy-text .dropdown-item').trigger('click') + }) + + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/c00000000c000000c0000\n' + + 'Testy transaction-link.send_you 75 Gradido.\n' + + '"Katzenauge, Eulenschrei, was verschwunden komm herbei!"\n' + + 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate\n' + + 'gdd_per_link.link-hint', + ) + }) + it('toasts success message', () => { + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied') + }) + }) + + describe('copy link with error', () => { beforeEach(async () => { navigatorClipboardMock.mockRejectedValue() await wrapper.find('.test-copy-link .dropdown-item').trigger('click') @@ -103,6 +124,17 @@ describe('TransactionLink', () => { expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied') }) }) + + describe('copy link and text with error', () => { + beforeEach(async () => { + navigatorClipboardMock.mockRejectedValue() + await wrapper.find('.test-copy-text .dropdown-item').trigger('click') + }) + + it('toasts an error', () => { + expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied') + }) + }) }) describe('qr code modal', () => { diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index c7b7682ec..76f705e35 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -18,9 +18,17 @@ - + - {{ $t('gdd_per_link.copy') }} + {{ $t('gdd_per_link.copy-link') }} + + + + {{ $t('gdd_per_link.copy-link-with-text') }} { - this.toastSuccess(this.$t('gdd_per_link.link-copied')) - }) - .catch(() => { - this.$bvModal.show('modalPopoverCopyError' + this.id) - this.toastError(this.$t('gdd_per_link.not-copied')) - }) - }, deleteLink() { this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => { if (value) diff --git a/frontend/src/components/UserSettings/UserPassword.vue b/frontend/src/components/UserSettings/UserPassword.vue index 0ba1576e8..2f571d400 100644 --- a/frontend/src/components/UserSettings/UserPassword.vue +++ b/frontend/src/components/UserSettings/UserPassword.vue @@ -6,6 +6,7 @@ {{ $t('settings.password.change-password') }} @@ -36,6 +37,7 @@ :variant="disabled ? 'light' : 'success'" class="mt-4" :disabled="disabled" + data-test="submit-new-password-btn" > {{ $t('form.save') }} diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index 917a25d70..5ab5f2392 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -8,7 +8,7 @@ const constants = { DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v2.2022-04-07', + EXPECTED: 'v3.2022-09-16', CURRENT: '', }, } @@ -60,6 +60,10 @@ const meta = { META_AUTHOR: process.env.META_AUTHOR || 'Bernd Hückstädt - Gradido-Akademie', } +const supportmail = { + SUPPORT_MAIL: process.env.SUPPORT_MAIL || 'support@supportmail.com', +} + // Check config version constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT if ( @@ -79,6 +83,7 @@ const CONFIG = { ...endpoints, ...community, ...meta, + ...supportmail, } module.exports = CONFIG diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 9b035cba6..3156c2861 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -74,6 +74,9 @@ export const createTransactionLink = gql` mutation($amount: Decimal!, $memo: String!) { createTransactionLink(amount: $amount, memo: $memo) { link + amount + memo + validUntil } } ` @@ -89,3 +92,71 @@ export const redeemTransactionLink = gql` redeemTransactionLink(code: $code) } ` + +export const createContribution = gql` + mutation($creationDate: String!, $memo: String!, $amount: Decimal!) { + createContribution(creationDate: $creationDate, memo: $memo, amount: $amount) { + amount + memo + } + } +` + +export const updateContribution = gql` + mutation($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) { + updateContribution( + contributionId: $contributionId + amount: $amount + memo: $memo + creationDate: $creationDate + ) { + id + amount + memo + } + } +` + +export const deleteContribution = gql` + mutation($id: Int!) { + deleteContribution(id: $id) + } +` + +export const createContributionMessage = gql` + mutation($contributionId: Float!, $message: String!) { + createContributionMessage(contributionId: $contributionId, message: $message) { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + } + } +` + +export const login = gql` + mutation($email: String!, $password: String!, $publisherId: Int) { + login(email: $email, password: $password, publisherId: $publisherId) { + email + firstName + lastName + language + klickTipp { + newsletterState + } + hasElopage + publisherId + isAdmin + creation + } + } +` + +export const logout = gql` + mutation { + logout + } +` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index adcd653a4..1c910a23e 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -1,22 +1,5 @@ import gql from 'graphql-tag' -export const login = gql` - query($email: String!, $password: String!, $publisherId: Int) { - login(email: $email, password: $password, publisherId: $publisherId) { - email - firstName - lastName - language - klickTipp { - newsletterState - } - hasElopage - publisherId - isAdmin - } - } -` - export const verifyLogin = gql` query { verifyLogin { @@ -30,16 +13,11 @@ export const verifyLogin = gql` hasElopage publisherId isAdmin + creation } } ` -export const logout = gql` - query { - logout - } -` - export const transactionsQuery = gql` query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) { @@ -59,6 +37,7 @@ export const transactionsQuery = gql` linkedUser { firstName lastName + email } decay { decay @@ -66,9 +45,6 @@ export const transactionsQuery = gql` end duration } - linkedUser { - email - } transactionLinkId } } @@ -160,3 +136,121 @@ export const listTransactionLinks = gql` } } ` + +export const listContributionLinks = gql` + query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + links { + id + amount + name + memo + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + count + } + } +` + +export const listContributions = gql` + query( + $currentPage: Int = 1 + $pageSize: Int = 25 + $order: Order = DESC + $filterConfirmed: Boolean = false + ) { + listContributions( + currentPage: $currentPage + pageSize: $pageSize + order: $order + filterConfirmed: $filterConfirmed + ) { + contributionCount + contributionList { + id + amount + memo + createdAt + contributionDate + confirmedAt + confirmedBy + deletedAt + state + messagesCount + } + } + } +` + +export const listAllContributions = gql` + query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + contributionCount + contributionList { + id + firstName + lastName + amount + memo + createdAt + contributionDate + confirmedAt + confirmedBy + } + } + } +` + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` + +export const searchAdminUsers = gql` + query { + searchAdminUsers { + userCount + userList { + firstName + lastName + } + } + } +` + +export const listContributionMessages = gql` + query($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) { + listContributionMessages( + contributionId: $contributionId + pageSize: $pageSize + currentPage: $currentPage + order: $order + ) { + count + messages { + id + message + createdAt + updatedAt + type + userFirstName + userLastName + userId + } + } + } +` diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js index f4f969008..243c35a24 100644 --- a/frontend/src/i18n.js +++ b/frontend/src/i18n.js @@ -1,9 +1,6 @@ import Vue from 'vue' import VueI18n from 'vue-i18n' -import en from 'vee-validate/dist/locale/en' -import de from 'vee-validate/dist/locale/de' - Vue.use(VueI18n) function loadLocaleMessages() { @@ -13,18 +10,9 @@ function loadLocaleMessages() { const matched = key.match(/([A-Za-z0-9-_]+)\./i) if (matched && matched.length > 1) { const locale = matched[1] - messages[locale] = locales(key) - if (locale === 'de') { - messages[locale] = { - validations: de, - ...messages[locale], - } - } - if (locale === 'en') { - messages[locale] = { - validations: en, - ...messages[locale], - } + messages[locale] = { + validations: require(`vee-validate/dist/locale/${locale}`), + ...locales(key), } } }) @@ -58,6 +46,45 @@ const numberFormats = { useGrouping: false, }, }, + es: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, + fr: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, + nl: { + decimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ungroupedDecimal: { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + }, + }, } const dateTimeFormats = { @@ -75,6 +102,19 @@ const dateTimeFormats = { hour: 'numeric', minute: 'numeric', }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, }, de: { short: { @@ -90,6 +130,103 @@ const dateTimeFormats = { hour: 'numeric', minute: 'numeric', }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + es: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + fr: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, + }, + nl: { + short: { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + hour: 'numeric', + minute: 'numeric', + }, + monthShort: { + month: 'short', + }, + month: { + month: 'long', + }, + year: { + year: 'numeric', + }, + monthAndYear: { + month: 'long', + year: 'numeric', + }, }, } diff --git a/frontend/src/layouts/AuthLayout.spec.js b/frontend/src/layouts/AuthLayout.spec.js index 30c858ea5..8d9411a71 100644 --- a/frontend/src/layouts/AuthLayout.spec.js +++ b/frontend/src/layouts/AuthLayout.spec.js @@ -19,6 +19,7 @@ describe('AuthLayout', () => { meta: { requiresAuth: false, }, + params: {}, }, } @@ -36,20 +37,6 @@ describe('AuthLayout', () => { wrapper = Wrapper() }) - describe('Mobile Version Start', () => { - beforeEach(() => { - wrapper.findComponent({ name: 'AuthMobileStart' }).vm.$emit('set-mobile-start', true) - }) - - it('has Component AuthMobileStart', () => { - expect(wrapper.findComponent({ name: 'AuthMobileStart' }).exists()).toBe(true) - }) - - it('has Component AuthNavbarSmall', () => { - expect(wrapper.findComponent({ name: 'AuthNavbarSmall' }).exists()).toBe(true) - }) - }) - describe('Desktop Version Start', () => { beforeEach(() => { wrapper.vm.mobileStart = false diff --git a/frontend/src/layouts/AuthLayout.vue b/frontend/src/layouts/AuthLayout.vue index 9c33ad195..948775952 100644 --- a/frontend/src/layouts/AuthLayout.vue +++ b/frontend/src/layouts/AuthLayout.vue @@ -1,10 +1,5 @@ diff --git a/frontend/src/pages/InfoStatistic.spec.js b/frontend/src/pages/InfoStatistic.spec.js new file mode 100644 index 000000000..e6475ed41 --- /dev/null +++ b/frontend/src/pages/InfoStatistic.spec.js @@ -0,0 +1,127 @@ +import { mount } from '@vue/test-utils' +import InfoStatistic from './InfoStatistic' +import { toastErrorSpy } from '../../test/testSetup' +import { listContributionLinks, communityStatistics, searchAdminUsers } from '@/graphql/queries' + +const localVue = global.localVue + +const apolloQueryMock = jest + .fn() + .mockResolvedValueOnce({ + data: { + listContributionLinks: { + count: 2, + links: [ + { + id: 1, + amount: 200, + name: 'Dokumenta 2017', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017', + cycle: 'ONCE', + }, + { + id: 2, + amount: 200, + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + cycle: 'ONCE', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + searchAdminUsers: { + userCount: 2, + userList: [ + { firstName: 'Peter', lastName: 'Lustig' }, + { firstName: 'Super', lastName: 'Admin' }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + communityStatistics: { + totalUsers: 3113, + // activeUsers: 1057, + // deletedUsers: 35, + totalGradidoCreated: '4083774.05000000000000000000', + totalGradidoDecayed: '-1062639.13634129622923372197', + totalGradidoAvailable: '2513565.869444365732411569', + // totalGradidoUnbookedDecayed: '-500474.6738366222166261272', + }, + }, + }) + .mockResolvedValue('default') + +describe('InfoStatistic', () => { + let wrapper + + const mocks = { + $i18n: { + locale: 'en', + }, + $t: jest.fn((t) => t), + $apollo: { + query: apolloQueryMock, + }, + } + + const Wrapper = () => { + return mount(InfoStatistic, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the info page', () => { + expect(wrapper.find('div.info-statistic').exists()).toBe(true) + }) + + it('calls listContributionLinks', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listContributionLinks, + }), + ) + }) + + it('calls searchAdminUsers', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: searchAdminUsers, + }), + ) + }) + + it.skip('calls getCommunityStatistics', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: communityStatistics, + }), + ) + }) + + describe('error apolloQueryMock', () => { + beforeEach(async () => { + jest.clearAllMocks() + apolloQueryMock.mockRejectedValue({ + message: 'uups', + }) + wrapper = Wrapper() + }) + + it('toasts two error messages', () => { + expect(toastErrorSpy).toBeCalledWith( + 'listContributionLinks has no result, use default data', + ) + expect(toastErrorSpy).toBeCalledWith('searchAdminUsers has no result, use default data') + // expect(toastErrorSpy).toBeCalledWith('communityStatistics has no result, use default data') + }) + }) + }) +}) diff --git a/frontend/src/pages/InfoStatistic.vue b/frontend/src/pages/InfoStatistic.vue new file mode 100644 index 000000000..de4b9e224 --- /dev/null +++ b/frontend/src/pages/InfoStatistic.vue @@ -0,0 +1,154 @@ + + diff --git a/frontend/src/pages/Login.spec.js b/frontend/src/pages/Login.spec.js index 6359d07c6..90e98cd44 100644 --- a/frontend/src/pages/Login.spec.js +++ b/frontend/src/pages/Login.spec.js @@ -5,7 +5,7 @@ import Login from './Login' const localVue = global.localVue -const apolloQueryMock = jest.fn() +const apolloMutateMock = jest.fn() const mockStoreDispach = jest.fn() const mockStoreCommit = jest.fn() const mockRouterPush = jest.fn() @@ -41,7 +41,7 @@ describe('Login', () => { params: {}, }, $apollo: { - query: apolloQueryMock, + mutate: apolloMutateMock, }, } @@ -113,7 +113,7 @@ describe('Login', () => { await wrapper.find('input[placeholder="Email"]').setValue('user@example.org') await wrapper.find('input[placeholder="form.password"]').setValue('1234') await flushPromises() - apolloQueryMock.mockResolvedValue({ + apolloMutateMock.mockResolvedValue({ data: { login: 'token', }, @@ -123,7 +123,7 @@ describe('Login', () => { }) it('calls the API with the given data', () => { - expect(apolloQueryMock).toBeCalledWith( + expect(apolloMutateMock).toBeCalledWith( expect.objectContaining({ variables: { email: 'user@example.org', @@ -175,7 +175,7 @@ describe('Login', () => { describe('login fails', () => { const createError = async (errorMessage) => { - apolloQueryMock.mockRejectedValue({ + apolloMutateMock.mockRejectedValue({ message: errorMessage, }) wrapper = Wrapper() diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 6a3db4e39..0b602f74b 100755 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -43,7 +43,7 @@ import InputPassword from '@/components/Inputs/InputPassword' import InputEmail from '@/components/Inputs/InputEmail' import Message from '@/components/Message/Message' -import { login } from '@/graphql/queries' +import { login } from '@/graphql/mutations' export default { name: 'Login', @@ -71,14 +71,13 @@ export default { container: this.$refs.submitButton, }) this.$apollo - .query({ - query: login, + .mutate({ + mutation: login, variables: { email: this.form.email, password: this.form.password, publisherId: this.$store.state.publisherId, }, - fetchPolicy: 'network-only', }) .then(async (result) => { const { diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index 47a30ff65..c1b6fb635 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -25,9 +25,11 @@ describe('Send', () => { const mocks = { $t: jest.fn((t) => t), $n: jest.fn((n) => String(n)), + $d: jest.fn((d) => d), $store: { state: { email: 'sender@example.org', + firstName: 'Testy', }, }, $apollo: { @@ -160,11 +162,15 @@ describe('Send', () => { }) describe('transaction form link', () => { + const now = new Date().toISOString() beforeEach(async () => { apolloMutationMock.mockResolvedValue({ data: { createTransactionLink: { link: 'http://localhost/redeem/0123456789', + amount: '56.78', + memo: 'Make the best of the link!', + validUntil: now, }, }, }) @@ -228,18 +234,65 @@ describe('Send', () => { navigator.clipboard = navigatorClipboard }) - describe('copy with success', () => { + describe('copy link with success', () => { beforeEach(async () => { navigatorClipboardMock.mockResolvedValue() - await wrapper.findAll('button').at(0).trigger('click') + await wrapper.findAll('button').at(1).trigger('click') }) + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/0123456789', + ) + }) it('toasts success message', () => { expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied') }) }) - describe('copy with error', () => { + describe('copy link with error', () => { + beforeEach(async () => { + navigatorClipboardMock.mockRejectedValue() + await wrapper.findAll('button').at(1).trigger('click') + }) + + it('toasts error message', () => { + expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied') + }) + }) + }) + + describe('copy link and text with success', () => { + const navigatorClipboard = navigator.clipboard + beforeAll(() => { + delete navigator.clipboard + navigator.clipboard = { writeText: navigatorClipboardMock } + }) + afterAll(() => { + navigator.clipboard = navigatorClipboard + }) + + describe('copy link and text with success', () => { + beforeEach(async () => { + navigatorClipboardMock.mockResolvedValue() + await wrapper.findAll('button').at(0).trigger('click') + }) + + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/0123456789\n' + + 'Testy transaction-link.send_you 56.78 Gradido.\n' + + '"Make the best of the link!"\n' + + 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate\n' + + 'gdd_per_link.link-hint', + ) + }) + it('toasts success message', () => { + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied') + }) + }) + + describe('copy link and text with error', () => { beforeEach(async () => { navigatorClipboardMock.mockRejectedValue() await wrapper.findAll('button').at(0).trigger('click') @@ -253,7 +306,7 @@ describe('Send', () => { describe('close button click', () => { beforeEach(async () => { - await wrapper.findAll('button').at(2).trigger('click') + await wrapper.findAll('button').at(3).trigger('click') }) it('Shows the TransactionForm', () => { diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index cd5f8f572..74e2b0270 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -41,7 +41,13 @@ >
@@ -144,7 +150,15 @@ export default { }) .then((result) => { this.$emit('set-tunneled-email', null) - this.link = result.data.createTransactionLink.link + const { + data: { + createTransactionLink: { link, amount, memo, validUntil }, + }, + } = result + this.link = link + this.amount = amount + this.memo = memo + this.validUntil = validUntil this.transactionData = { ...EMPTY_TRANSACTION_DATA } this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink this.updateTransactions({}) diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index b1bbd8950..adbb25226 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -43,6 +43,7 @@ const mocks = { $store: { state: { token: null, + tokenTime: null, email: 'bibi@bloxberg.de', }, }, @@ -68,7 +69,7 @@ describe('TransactionLink', () => { } describe('mount', () => { - beforeEach(() => { + beforeAll(() => { jest.clearAllMocks() wrapper = Wrapper() }) @@ -214,148 +215,159 @@ describe('TransactionLink', () => { }) }) - describe('token in store and own link', () => { - beforeEach(() => { + describe('token in store', () => { + beforeAll(() => { mocks.$store.state.token = 'token' - apolloQueryMock.mockResolvedValue({ - data: { - queryTransactionLink: { - __typename: 'TransactionLink', - id: 92, - amount: '22', - memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', - createdAt: '2022-03-17T16:10:28.000Z', - validUntil: transactionLinkValidExpireDate(), - redeemedAt: null, - deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, - }, - }, - }) - wrapper = Wrapper() }) - it('has a RedeemSelfCreator component', () => { - expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true) - }) - - it('has a no redeem text', () => { - expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain( - 'gdd_per_link.no-redeem', - ) - }) - - it.skip('has a link to transaction page', () => { - expect(wrapper.find('a[target="/transactions"]').exists()).toBe(true) - }) - }) - - describe('valid link', () => { - beforeEach(() => { - mocks.$store.state.token = 'token' - apolloQueryMock.mockResolvedValue({ - data: { - queryTransactionLink: { - __typename: 'TransactionLink', - id: 92, - amount: '22', - memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', - createdAt: '2022-03-17T16:10:28.000Z', - validUntil: transactionLinkValidExpireDate(), - redeemedAt: null, - deletedAt: null, - user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' }, - }, - }, - }) - wrapper = Wrapper() - }) - - it('has a RedeemValid component', () => { - expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true) - }) - - it('has a button with redeem text', () => { - expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe( - 'gdd_per_link.redeem', - ) - }) - - describe('redeem link with success', () => { - let spy - - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockResolvedValue() - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + describe('sufficient token time in store', () => { + beforeAll(() => { + mocks.$store.state.tokenTime = Math.floor(Date.now() / 1000) + 20 }) - it('opens the modal', () => { - expect(spy).toBeCalledWith('gdd_per_link.redeem-text') - }) - - it('calls the API', () => { - expect(apolloMutateMock).toBeCalledWith( - expect.objectContaining({ - mutation: redeemTransactionLink, - variables: { - code: 'some-code', + describe('own link', () => { + beforeAll(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + __typename: 'TransactionLink', + id: 92, + amount: '22', + memo: + 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, }, - }), - ) + }) + wrapper = Wrapper() + }) + + it('has a RedeemSelfCreator component', () => { + expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).exists()).toBe(true) + }) + + it('has a no redeem text', () => { + expect(wrapper.findComponent({ name: 'RedeemSelfCreator' }).text()).toContain( + 'gdd_per_link.no-redeem', + ) + }) + + it.skip('has a link to transaction page', () => { + expect(wrapper.find('a[target="/transactions"]').exists()).toBe(true) + }) }) - it('toasts a success message', () => { - expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem') - expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ') - }) + describe('valid link', () => { + beforeAll(() => { + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + __typename: 'TransactionLink', + id: 92, + amount: '22', + memo: + 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' }, + }, + }, + }) + wrapper = Wrapper() + }) - it('pushes the route to overview', () => { - expect(routerPushMock).toBeCalledWith('/overview') + it('has a RedeemValid component', () => { + expect(wrapper.findComponent({ name: 'RedeemValid' }).exists()).toBe(true) + }) + + it('has a button with redeem text', () => { + expect(wrapper.findComponent({ name: 'RedeemValid' }).find('button').text()).toBe( + 'gdd_per_link.redeem', + ) + }) + + describe('redeem link with success', () => { + beforeAll(async () => { + apolloMutateMock.mockResolvedValue() + await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + }) + + it('calls the API', () => { + expect(apolloMutateMock).toBeCalledWith( + expect.objectContaining({ + mutation: redeemTransactionLink, + variables: { + code: 'some-code', + }, + }), + ) + }) + + it('toasts a success message', () => { + expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem') + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ') + }) + + it('pushes the route to overview', () => { + expect(routerPushMock).toBeCalledWith('/overview') + }) + }) + + describe('redeem link with error', () => { + beforeAll(async () => { + apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) + await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Oh Noo!') + }) + + it('pushes the route to overview', () => { + expect(routerPushMock).toBeCalledWith('/overview') + }) + }) }) }) - describe('cancel redeem link', () => { - let spy - - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockResolvedValue() - spy.mockImplementation(() => Promise.resolve(false)) - await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + describe('no sufficient token time in store', () => { + beforeAll(() => { + mocks.$store.state.tokenTime = 1665125185 + apolloQueryMock.mockResolvedValue({ + data: { + queryTransactionLink: { + __typename: 'TransactionLink', + id: 92, + amount: '22', + memo: + 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', + createdAt: '2022-03-17T16:10:28.000Z', + validUntil: transactionLinkValidExpireDate(), + redeemedAt: null, + deletedAt: null, + user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + }, + }, + }) + wrapper = Wrapper() }) - it('does not call the API', () => { - expect(apolloMutateMock).not.toBeCalled() + it('has a RedeemLoggedOut component', () => { + expect(wrapper.findComponent({ name: 'RedeemLoggedOut' }).exists()).toBe(true) }) - it('does not toasts a success message', () => { - expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) - expect(toastSuccessSpy).not.toBeCalled() + it('has a link to register with code', () => { + expect(wrapper.find('a[href="/register/some-code"]').exists()).toBe(true) }) - it('does not push the route', () => { - expect(routerPushMock).not.toBeCalled() - }) - }) - - describe('redeem link with error', () => { - let spy - - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) - spy.mockImplementation(() => Promise.resolve(true)) - await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Oh Noo!') - }) - - it('pushes the route to overview', () => { - expect(routerPushMock).toBeCalledWith('/overview') + it('has a link to login with code', () => { + expect(wrapper.find('a[href="/login/some-code"]').exists()).toBe(true) }) }) }) diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 699c350ae..c3875d20e 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -14,7 +14,7 @@ @@ -98,18 +98,19 @@ export default { this.$router.push('/overview') }) }, - redeemLink(amount) { - this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => { - if (value) this.mutationLink(amount) - }) - }, }, computed: { isContributionLink() { return this.$route.params.code.search(/^CL-/) === 0 }, + tokenExpiresInSeconds() { + const remainingSecs = Math.floor( + (new Date(this.$store.state.tokenTime * 1000).getTime() - new Date().getTime()) / 1000, + ) + return remainingSecs <= 0 ? 0 : remainingSecs + }, itemType() { - // link wurde gelöscht: am, von + // link is deleted: at, from if (this.linkData.deletedAt) { // eslint-disable-next-line vue/no-side-effects-in-computed-properties this.redeemedBoxText = this.$t('gdd_per_link.link-deleted', { @@ -135,7 +136,9 @@ export default { return `TEXT` } - if (this.$store.state.token) { + if (this.$store.state.token && this.$store.state.tokenTime) { + if (this.tokenExpiresInSeconds < 5) return `LOGGED_OUT` + // logged in, nicht berechtigt einzulösen, eigener link if (this.linkData.user && this.$store.state.email === this.linkData.user.email) { return `SELF_CREATOR` diff --git a/frontend/src/pages/Transactions.spec.js b/frontend/src/pages/Transactions.spec.js index 94e0f51c1..171a089ca 100644 --- a/frontend/src/pages/Transactions.spec.js +++ b/frontend/src/pages/Transactions.spec.js @@ -6,6 +6,7 @@ import { toastErrorSpy } from '@test/testSetup' const localVue = global.localVue +const mockRouterReplace = jest.fn() const windowScrollToMock = jest.fn() window.scrollTo = windowScrollToMock @@ -39,6 +40,9 @@ describe('Transactions', () => { $apollo: { query: apolloMock, }, + $router: { + replace: mockRouterReplace, + }, } const Wrapper = () => { diff --git a/frontend/src/pages/Transactions.vue b/frontend/src/pages/Transactions.vue index 109e3f19c..d0f4ac8b8 100644 --- a/frontend/src/pages/Transactions.vue +++ b/frontend/src/pages/Transactions.vue @@ -1,7 +1,11 @@