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/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..2dc9fb3ce --- /dev/null +++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js @@ -0,0 +1,242 @@ +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?', + ) + }) + }) + }) + + describe('links in contribtion message', () => { + 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, + }) + } + + let messageField + + describe('message of only one link', () => { + beforeEach(() => { + propsData.message.message = 'https://gradido.net/de/' + wrapper = ModeratorItemWrapper() + messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)') + }) + + it('contains the link as text', () => { + expect(messageField.text()).toBe('https://gradido.net/de/') + }) + + it('contains a link to the given address', () => { + expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/') + }) + }) + + describe('message with text and two links', () => { + beforeEach(() => { + propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/ +and here is the link to the repository: https://github.com/gradido/gradido` + wrapper = ModeratorItemWrapper() + messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)') + }) + + it('contains the whole text', () => { + expect(messageField.text()) + .toBe(`Here you find all you need to know about Gradido: https://gradido.net/de/ +and here is the link to the repository: https://github.com/gradido/gradido`) + }) + + it('contains the two links', () => { + expect(messageField.findAll('a').at(0).attributes('href')).toBe('https://gradido.net/de/') + expect(messageField.findAll('a').at(1).attributes('href')).toBe( + 'https://github.com/gradido/gradido', + ) + }) + }) + }) +}) diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue new file mode 100644 index 000000000..9c7a3a0f2 --- /dev/null +++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue @@ -0,0 +1,61 @@ + + + + diff --git a/frontend/src/components/ContributionMessages/LinkifyMessage.vue b/frontend/src/components/ContributionMessages/LinkifyMessage.vue new file mode 100644 index 000000000..5d6ec34cb --- /dev/null +++ b/frontend/src/components/ContributionMessages/LinkifyMessage.vue @@ -0,0 +1,37 @@ + + + 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..71593f2b1 --- /dev/null +++ b/frontend/src/components/Contributions/ContributionForm.vue @@ -0,0 +1,176 @@ + + + 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 e88050047..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/TransactionLinks/TransactionLink.spec.js b/frontend/src/components/TransactionLinks/TransactionLink.spec.js index d06f0f726..798223f60 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.spec.js +++ b/frontend/src/components/TransactionLinks/TransactionLink.spec.js @@ -105,7 +105,8 @@ describe('TransactionLink', () => { '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', + 'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate\n' + + 'gdd_per_link.link-hint', ) }) it('toasts success message', () => { diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index 5618c8696..76f705e35 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -18,17 +18,17 @@ - + - {{ $t('gdd_per_link.copy') }} + {{ $t('gdd_per_link.copy-link') }} - {{ $t('gdd_per_link.copy-with-text') }} + {{ $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')) - }) - }, - copyLinkWithText() { - navigator.clipboard - .writeText( - `${this.link} -${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido. -"${this.memo}" -${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', { - date: this.$d(new Date(this.validUntil), 'short'), - })}`, - ) - .then(() => { - this.toastSuccess(this.$t('gdd_per_link.link-and-text-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 27e63d568..1c910a23e 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -1,23 +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 - creation - } - } -` - export const verifyLogin = gql` query { verifyLogin { @@ -36,12 +18,6 @@ export const verifyLogin = gql` } ` -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) { @@ -61,6 +37,7 @@ export const transactionsQuery = gql` linkedUser { firstName lastName + email } decay { decay @@ -68,9 +45,6 @@ export const transactionsQuery = gql` end duration } - linkedUser { - email - } transactionLinkId } } @@ -162,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 46f58a4a2..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 @@