diff --git a/admin/jest.config.js b/admin/jest.config.js index 9b9842bad..3e416e7b6 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', // not 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 5da80bd1f..98746be6d 100644 --- a/admin/package.json +++ b/admin/package.json @@ -70,7 +70,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/yarn.lock b/admin/yarn.lock index 09b543354..b5b76cee8 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -1282,17 +1282,6 @@ jest-message-util "^24.9.0" jest-mock "^24.9.0" -"@jest/fake-timers@^25.1.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.5.0.tgz#46352e00533c024c90c2bc2ad9f2959f7f114185" - integrity sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ== - dependencies: - "@jest/types" "^25.5.0" - jest-message-util "^25.5.0" - jest-mock "^25.5.0" - jest-util "^25.5.0" - lolex "^5.0.0" - "@jest/fake-timers@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" @@ -1504,16 +1493,6 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@jest/types@^25.5.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" - integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -4130,14 +4109,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -8012,16 +7983,6 @@ jest-environment-jsdom-fifteen@^1.0.2: jest-util "^24.0.0" jsdom "^15.2.1" -jest-environment-jsdom-sixteen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom-sixteen/-/jest-environment-jsdom-sixteen-2.0.0.tgz#0f8c12663ccd9836d248574decffc575bfb091e1" - integrity sha512-BF+8P67aEJcd78TQzwSb9P4a73cArOWb5KgqI8eU6cHRWDIJdDRE8XTeZAmOuDSDhKpuEXjKkXwWB3GOJvqHJQ== - dependencies: - "@jest/fake-timers" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jsdom "^16.2.1" - jest-environment-jsdom@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" @@ -8236,20 +8197,6 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-message-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.5.0.tgz#ea11d93204cc7ae97456e1d8716251185b8880ea" - integrity sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^25.5.0" - "@types/stack-utils" "^1.0.1" - chalk "^3.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - slash "^3.0.0" - stack-utils "^1.0.1" - jest-message-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" @@ -8272,13 +8219,6 @@ jest-mock@^24.0.0, jest-mock@^24.9.0: dependencies: "@jest/types" "^24.9.0" -jest-mock@^25.1.0, jest-mock@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" - integrity sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA== - dependencies: - "@jest/types" "^25.5.0" - jest-mock@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" @@ -8555,17 +8495,6 @@ jest-util@^24.0.0, jest-util@^24.9.0: slash "^2.0.0" source-map "^0.6.0" -jest-util@^25.1.0, jest-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" - integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== - dependencies: - "@jest/types" "^25.5.0" - chalk "^3.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - make-dir "^3.0.0" - jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" @@ -8812,7 +8741,7 @@ jsdom@^15.2.1: ws "^7.0.0" xml-name-validator "^3.0.0" -jsdom@^16.2.1, jsdom@^16.4.0: +jsdom@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== @@ -9167,13 +9096,6 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== -lolex@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index f44aa584c..66bea06f5 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0044-insert_missing_contributions', + DB_VERSION: '0045-add_denied_type_and_status_to_contributions', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/enum/ContributionStatus.ts b/backend/src/graphql/enum/ContributionStatus.ts new file mode 100644 index 000000000..67cdf5398 --- /dev/null +++ b/backend/src/graphql/enum/ContributionStatus.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +export enum ContributionStatus { + PENDING = 'PENDING', + DELETED = 'DELETED', + IN_PROGRESS = 'IN_PROGRESS', + DENIED = 'DENIED', + CONFIRMED = 'CONFIRMED', +} + +registerEnumType(ContributionStatus, { + name: 'ContributionStatus', + description: 'Name of the Type of the Contribution Status', +}) diff --git a/backend/src/graphql/enum/ContributionType.ts b/backend/src/graphql/enum/ContributionType.ts new file mode 100644 index 000000000..e8529edc4 --- /dev/null +++ b/backend/src/graphql/enum/ContributionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum ContributionType { + ADMIN = 'ADMIN', + USER = 'USER', + LINK = 'LINK', +} + +registerEnumType(ContributionType, { + name: 'ContributionType', + description: 'Name of the Type of the Contribution', +}) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index b7ac63842..e70fe71ee 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -36,6 +36,8 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User as dbUser } from '@entity/User' import { User } from '@model/User' import { TransactionTypeId } from '@enum/TransactionTypeId' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import Decimal from 'decimal.js-light' import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' @@ -260,6 +262,8 @@ export class AdminResolver { contribution.contributionDate = creationDateObj contribution.memo = memo contribution.moderatorId = moderator.id + contribution.contributionType = ContributionType.ADMIN + contribution.contributionStatus = ContributionStatus.PENDING logger.trace('contribution to save', contribution) await Contribution.save(contribution) @@ -337,6 +341,7 @@ export class AdminResolver { contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) contributionToUpdate.moderatorId = moderator.id + contributionToUpdate.contributionStatus = ContributionStatus.PENDING await Contribution.save(contributionToUpdate) const result = new AdminUpdateContribution() @@ -387,6 +392,8 @@ export class AdminResolver { if (!contribution) { throw new Error('Contribution not found for given id.') } + contribution.contributionStatus = ContributionStatus.DELETED + await contribution.save() const res = await contribution.softRemove() return !!res } @@ -454,6 +461,7 @@ export class AdminResolver { contribution.confirmedAt = receivedCallDate contribution.confirmedBy = moderatorUser.id contribution.transactionId = transaction.id + contribution.contributionStatus = ContributionStatus.CONFIRMED await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution) await queryRunner.commitTransaction() diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 3307252e4..8056ffde3 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -7,6 +7,8 @@ import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm' import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { User } from '@model/User' @@ -43,6 +45,8 @@ export class ContributionResolver { contribution.createdAt = new Date() contribution.contributionDate = creationDateObj contribution.memo = memo + contribution.contributionType = ContributionType.USER + contribution.contributionStatus = ContributionStatus.PENDING logger.trace('contribution to save', contribution) await dbContribution.save(contribution) @@ -66,6 +70,8 @@ export class ContributionResolver { if (contribution.confirmedAt) { throw new Error('A confirmed contribution can not be deleted') } + contribution.contributionStatus = ContributionStatus.DELETED + await contribution.save() const res = await contribution.softRemove() return !!res } @@ -164,6 +170,7 @@ export class ContributionResolver { contributionToUpdate.amount = amount contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) + contributionToUpdate.contributionStatus = ContributionStatus.PENDING dbContribution.save(contributionToUpdate) return new UnconfirmedContribution(contributionToUpdate, user, creations) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8696065ed..ccc0f628d 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -26,6 +26,8 @@ import { User } from '@model/User' import { calculateDecay } from '@/util/decay' import { executeTransaction } from './TransactionResolver' import { Order } from '@enum/Order' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { getUserCreation, validateContribution } from './util/creations' @@ -231,6 +233,9 @@ export class TransactionLinkResolver { contribution.memo = contributionLink.memo contribution.amount = contributionLink.amount contribution.contributionLinkId = contributionLink.id + contribution.contributionType = ContributionType.LINK + contribution.contributionStatus = ContributionStatus.CONFIRMED + await queryRunner.manager.insert(DbContribution, contribution) const lastTransaction = await queryRunner.manager diff --git a/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts b/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts new file mode 100644 index 000000000..c376ae53e --- /dev/null +++ b/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts @@ -0,0 +1,83 @@ +import Decimal from 'decimal.js-light' +import { + BaseEntity, + Column, + Entity, + PrimaryGeneratedColumn, + DeleteDateColumn, + JoinColumn, + ManyToOne, +} from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { User } from '../User' + +@Entity('contributions') +export class Contribution extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ unsigned: true, nullable: false, name: 'user_id' }) + userId: number + + @ManyToOne(() => User, (user) => user.contributions) + @JoinColumn({ name: 'user_id' }) + user: User + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) + createdAt: Date + + @Column({ type: 'datetime', nullable: false, name: 'contribution_date' }) + contributionDate: Date + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ unsigned: true, nullable: true, name: 'moderator_id' }) + moderatorId: number + + @Column({ unsigned: true, nullable: true, name: 'contribution_link_id' }) + contributionLinkId: number + + @Column({ unsigned: true, nullable: true, name: 'confirmed_by' }) + confirmedBy: number + + @Column({ nullable: true, name: 'confirmed_at' }) + confirmedAt: Date + + @Column({ unsigned: true, nullable: true, name: 'denied_by' }) + deniedBy: number + + @Column({ nullable: true, name: 'denied_at' }) + deniedAt: Date + + @Column({ + name: 'contribution_type', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionType: string + + @Column({ + name: 'contribution_status', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionStatus: string + + @Column({ unsigned: true, nullable: true, name: 'transaction_id' }) + transactionId: number + + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt: Date | null +} diff --git a/database/entity/Contribution.ts b/database/entity/Contribution.ts index 82dd6478c..800e7f9cd 100644 --- a/database/entity/Contribution.ts +++ b/database/entity/Contribution.ts @@ -1 +1 @@ -export { Contribution } from './0039-contributions_table/Contribution' +export { Contribution } from './0045-add_denied_type_and_status_to_contributions/Contribution' diff --git a/database/migrations/0045-add_denied_type_and_status_to_contributions.ts b/database/migrations/0045-add_denied_type_and_status_to_contributions.ts new file mode 100644 index 000000000..b3653589b --- /dev/null +++ b/database/migrations/0045-add_denied_type_and_status_to_contributions.ts @@ -0,0 +1,39 @@ +/* MIGRATION TO ADD denied_by, denied_at, contribution_type and contrinution_status +FIELDS TO contributions */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `denied_at` datetime DEFAULT NULL AFTER `confirmed_at`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `denied_by` int(10) unsigned DEFAULT NULL AFTER `denied_at`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `contribution_type` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "ADMIN" AFTER `denied_by`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `contribution_status` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "PENDING" AFTER `contribution_type`;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_type` = "LINK" WHERE `contribution_link_id` IS NOT NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_type` = "USER" WHERE `contribution_link_id` IS NULL AND `moderator_id` IS NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_status` = "CONFIRMED" WHERE `confirmed_at` IS NOT NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_status` = "DELETED" WHERE `deleted_at` IS NOT NULL;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `contributions` DROP COLUMN `contribution_status`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `contribution_type`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `denied_by`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `denied_at`;') +} diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 2a52ec707..a32330f3b 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -23,5 +23,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', // not needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen } diff --git a/frontend/package.json b/frontend/package.json index 71baf4764..8121e2ca2 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,6 @@ "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "jest-canvas-mock": "^2.3.1", - "jest-environment-jsdom-sixteen": "^2.0.0", "jwt-decode": "^3.1.2", "portal-vue": "^2.1.7", "prettier": "^2.2.1", diff --git a/frontend/src/components/Contributions/ContributionForm.spec.js b/frontend/src/components/Contributions/ContributionForm.spec.js index eb1509328..cf47577a3 100644 --- a/frontend/src/components/Contributions/ContributionForm.spec.js +++ b/frontend/src/components/Contributions/ContributionForm.spec.js @@ -46,46 +46,274 @@ describe('ContributionForm', () => { }) describe('empty form data', () => { - describe('buttons', () => { - it('has reset enabled', () => { - expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy() + 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) + }) + }) }) - it('has submit disabled', () => { - expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled') + 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', () => { + describe('fill in form data with "id === null"', () => { const now = new Date().toISOString() beforeEach(async () => { await wrapper.setData({ form: { id: null, - date: now, - memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', - amount: '200', + date: '', + memo: '', + amount: '', }, }) }) - describe('buttons', () => { - it('has reset enabled', () => { - expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy() + describe('invalid form data', () => { + beforeEach(async () => { + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper.find('#contribution-amount').find('input').setValue('200') }) - it('has submit enabled', () => { - expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy() + 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 and "id"', () => { + describe('fill in form data with set "id"', () => { const now = new Date().toISOString() beforeEach(async () => { @@ -93,19 +321,104 @@ describe('ContributionForm', () => { form: { id: 2, date: now, - memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...', - amount: '200', + memo: 'Mein kommerzieller Beitrag für diesen Monat ...', + amount: '100', }, }) }) - describe('buttons', () => { - it('has reset enabled', () => { - expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy() + describe('invalid form data', () => { + beforeEach(async () => { + await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now) + await wrapper.find('#contribution-amount').find('input').setValue('200') }) - it('has submit enabled', () => { - expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy() + 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 index 5fad059fb..3a9010ec2 100644 --- a/frontend/src/components/Contributions/ContributionForm.vue +++ b/frontend/src/components/Contributions/ContributionForm.vue @@ -4,8 +4,8 @@

{{ $t('contribution.formText.yourContribution') }}

{{ $t('contribution.formText.bringYourTalentsTo') }}
    -
  • -
  • +
  • +
@@ -73,12 +73,12 @@
- + {{ $t('form.cancel') }} - + {{ form.id ? $t('form.change') : $t('contribution.submit') }} @@ -108,11 +108,8 @@ export default { }, submit() { this.form.amount = this.numberFormat(this.form.amount) - if (this.form.id) { - this.$emit('update-contribution', this.form) - } else { - this.$emit('set-contribution', this.form) - } + // spreading is needed for testing + this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form }) this.reset() }, reset() { @@ -122,53 +119,47 @@ export default { this.form.memo = '' this.form.amount = '' }, + textForMonth(date, availableAmount) { + const obj = { + monthAndYear: this.$d(date, 'monthAndYear'), + creation: availableAmount, + } + return this.$t('contribution.formText.openAmountForMonth', obj) + }, }, computed: { - /* - * minimalDate() = Sets the date to the 1st of the previous month. - * - */ minimalDate() { - return new Date(this.maximalDate.getFullYear(), this.maximalDate.getMonth() - 1, 1) + // sets the date to the 1st of the previous month + let date = new Date(this.maximalDate) // has to be a new object, because of 'setMonth' changes the objects date + date = new Date(date.setMonth(date.getMonth() - 1)) + return new Date(date.getFullYear(), date.getMonth(), 1) }, disabled() { - if ( + return ( this.form.date === '' || this.form.memo.length < this.minlength || + this.form.memo.length > this.maxlength || parseInt(this.form.amount) <= 0 || parseInt(this.form.amount) > 1000 || (this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddThisMonth)) || (!this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddLastMonth)) ) - return true - return false - }, - lastMonthObject() { - // new Date().getMonth === 1 If the current month is January, then one year must be gone back in the previous month - const obj = { - monthAndYear: this.$d(new Date(this.minimalDate), 'monthAndYear'), - creation: this.maxGddLastMonth, - } - return this.$t('contribution.formText.openAmountForMonth', obj) - }, - thisMonthObject() { - const obj = { - monthAndYear: this.$d(new Date(), 'monthAndYear'), - creation: this.maxGddThisMonth, - } - return this.$t('contribution.formText.openAmountForMonth', obj) }, isThisMonth() { - return new Date(this.form.date).getMonth() === new Date().getMonth() + const formDate = new Date(this.form.date) + return ( + formDate.getFullYear() === this.maximalDate.getFullYear() && + formDate.getMonth() === this.maximalDate.getMonth() + ) }, maxGddLastMonth() { - // When edited, the amount is added back on top of the amount + // when existing contribution is edited, the amount is added back on top of the amount return this.form.id && !this.isThisMonth ? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount) : this.$store.state.creation[1] }, maxGddThisMonth() { - // When edited, the amount is added back on top of the amount + // when existing contribution is edited, the amount is added back on top of the amount return this.form.id && this.isThisMonth ? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount) : this.$store.state.creation[2] diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js index 10c1f3a9e..4f26b64cf 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js @@ -47,6 +47,7 @@ describe('GddSend confirm', () => { }) }) + describe('has totalBalance under 0', () => { beforeEach(async () => { await wrapper.setProps({ @@ -54,7 +55,7 @@ describe('GddSend confirm', () => { }) }) - it('has button disable', () => { + it('has send button disabled', () => { expect(wrapper.find('.send-button').attributes('disabled')).toBe('disabled') }) }) diff --git a/frontend/src/components/Menu/Navbar.spec.js b/frontend/src/components/Menu/Navbar.spec.js index df422bea2..8683d23a4 100644 --- a/frontend/src/components/Menu/Navbar.spec.js +++ b/frontend/src/components/Menu/Navbar.spec.js @@ -48,7 +48,7 @@ describe('Navbar', () => { expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy() }) - it('has ten b-nav-item in the navbar', () => { + it('has twelve b-nav-item in the navbar', () => { expect(wrapper.findAll('.nav-item')).toHaveLength(12) }) @@ -68,12 +68,16 @@ describe('Navbar', () => { expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions') }) - it('has first nav-item "navigation.transactions" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.community') + it('has first nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('gdt.gdt') + }) + + it('has first nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.community') }) it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.profile') + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.profile') }) it('has first nav-item "navigation.info" in navbar', () => { diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index fa02e2aaf..89176d098 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -52,6 +52,10 @@ {{ $t('navigation.transactions') }} + + + {{ $t('gdt.gdt') }} + {{ $t('navigation.community') }} diff --git a/frontend/src/components/Menu/Sidebar.spec.js b/frontend/src/components/Menu/Sidebar.spec.js index 873fa25b0..5f67f38a5 100644 --- a/frontend/src/components/Menu/Sidebar.spec.js +++ b/frontend/src/components/Menu/Sidebar.spec.js @@ -46,32 +46,37 @@ describe('Sidebar', () => { expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') }) - it('has first nav-item "navigation.transactions" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions') - }) - - it('has first nav-item "navigation.community" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(3).text()).toContain('navigation.community') - }) - - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.profile') - }) - - it('has first nav-item "navigation.info" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.info') - }) + it('has first nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('gdt.gdt') }) - describe('navigation Navbar (user has an elopage account)', () => { - it('has nine b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(9) - }) + it('has first nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.community') + }) - it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(6).find('a').attributes('href')).toBe('#') - }) + it('has first nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.profile') + }) + }) + + describe('navigation Navbar (user has an elopage account)', () => { + it('has eight b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(9) + }) + + it('has a link to the members area', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(6).find('a').attributes('href')).toBe('#') + }) + + it('has first nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') + }) + + it('has first nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.logout') + }) + }) it('has first nav-item "navigation.admin_area" in navbar', () => { expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') diff --git a/frontend/src/components/Menu/Sidebar.vue b/frontend/src/components/Menu/Sidebar.vue index 594748276..abd9b393c 100644 --- a/frontend/src/components/Menu/Sidebar.vue +++ b/frontend/src/components/Menu/Sidebar.vue @@ -16,6 +16,10 @@ {{ $t('navigation.transactions') }} + + + {{ $t('gdt.gdt') }} + {{ $t('navigation.community') }} 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 @@