diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e1cfbfd8ea..0748858133 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -61,4 +61,4 @@ Fixes # **Have you read the [contributing guide](https://github.com/PalisadoesFoundation/talawa-admin/blob/master/CONTRIBUTING.md)?** - + \ No newline at end of file diff --git a/.github/workflows/check-tsdoc.js b/.github/workflows/check-tsdoc.js index d5c3b33b90..0400f5a108 100644 --- a/.github/workflows/check-tsdoc.js +++ b/.github/workflows/check-tsdoc.js @@ -23,6 +23,7 @@ async function findTsxFiles(dir) { } else if ( filePath.endsWith('.tsx') && !filePath.endsWith('.test.tsx') && + !filePath.endsWith('.spec.tsx') && !filesToSkip.includes(path.relative(dir, filePath)) ) { results.push(filePath); diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7cb9c10f69..fb6505324e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -49,7 +49,7 @@ jobs: - name: Run formatting if check fails if: failure() - run: npm run format + run: npm run format:fix - name: Check for type errors if: steps.changed-files.outputs.only_changed != 'true' @@ -101,6 +101,7 @@ jobs: .node-version .husky/** scripts/** + src/style/** schema.graphql package.json tsconfig.json @@ -252,33 +253,34 @@ jobs: path: "./coverage/lcov.info" min_coverage: 0.0 - Graphql-Inspector: - if: ${{ github.actor != 'dependabot[bot]' }} - name: Runs Introspection on the GitHub talawa-api repo on the schema.graphql file - runs-on: ubuntu-latest - steps: - - name: Checkout the Repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22.x' - - - name: resolve dependency - run: npm install -g @graphql-inspector/cli + # Graphql-Inspector: + # if: ${{ github.actor != 'dependabot[bot]' }} + # name: Runs Introspection on the GitHub talawa-api repo on the schema.graphql file + # runs-on: ubuntu-latest + # steps: + # - name: Checkout the Repository + # uses: actions/checkout@v4 + + # - name: Set up Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: '22.x' + + # - name: resolve dependency + # run: npm install -g @graphql-inspector/cli - - name: Clone API Repository - run: | - # Retrieve the complete branch name directly from the GitHub context - FULL_BRANCH_NAME=${{ github.base_ref }} - echo "FULL_Branch_NAME: $FULL_BRANCH_NAME" + # - name: Clone API Repository + # run: | + # # Retrieve the complete branch name directly from the GitHub context + # FULL_BRANCH_NAME=${{ github.base_ref }} + # echo "FULL_Branch_NAME: $FULL_BRANCH_NAME" - # Clone the specified repository using the extracted branch name - git clone --branch $FULL_BRANCH_NAME https://github.com/PalisadoesFoundation/talawa-api && ls -a + # # Clone the specified repository using the extracted branch name + # git clone --branch $FULL_BRANCH_NAME https://github.com/PalisadoesFoundation/talawa-api && ls -a - - name: Validate Documents - run: graphql-inspector validate './src/GraphQl/**/*.ts' './talawa-api/schema.graphql' + # - name: Validate Documents + # run: graphql-inspector validate './src/GraphQl/**/*.ts' './talawa-api/schema.graphql' + Start-App-Without-Docker: name: Check if Talawa Admin app starts (No Docker) runs-on: ubuntu-latest diff --git a/README.md b/README.md index cbade9e407..911ba11453 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talawa Admin -[💬 Join the community on Slack](https://github.com/PalisadoesFoundation/) +💬 Join the community on Slack from our [Palisadoes Foundation GitHub Home Page](https://github.com/PalisadoesFoundation) ![talawa-logo-lite-200x200](https://github.com/PalisadoesFoundation/talawa-admin/assets/16875803/26291ec5-d3c1-4135-8bc7-80885dff613d) diff --git a/jest.config.js b/jest.config.js index 3083bcda4f..75e0cc5b4d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,8 @@ export default { '!**/index.{js,ts}', '!**/*.d.ts', '!src/test/**', - '!vitest.config.ts',], + '!vitest.config.ts', + ], // setupFiles: ['react-app-polyfill/jsdom'], setupFiles: ['whatwg-fetch'], setupFilesAfterEnv: ['/src/setupTests.ts'], @@ -35,6 +36,7 @@ export default { '/src', ], moduleNameMapper: { + '\\.(css|scss|sass|less)$': 'identity-obj-proxy', '^react-native$': 'react-native-web', '^@dicebear/core$': '/scripts/__mocks__/@dicebear/core.ts', '^@dicebear/collection$': @@ -42,6 +44,7 @@ export default { '\\.svg\\?react$': '/scripts/__mocks__/fileMock.js', '\\.svg$': '/scripts/__mocks__/fileMock.js', '^@pdfme/generator$': '/scripts/__mocks__/@pdfme/generator.ts', + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, moduleFileExtensions: [ 'web.js', diff --git a/package-lock.json b/package-lock.json index 2404c03835..07a65a79c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.6.1", - "inquirer": "^8.0.0", + "inquirer": "^11.0.2", "js-cookie": "^3.0.1", "lcov-result-merger": "^5.0.1", "markdown-toc": "^1.2.0", @@ -3157,6 +3157,239 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", + "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5704,11 +5937,19 @@ "@types/unist": "*" } }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", - "devOptional": true, "dependencies": { "undici-types": "~6.19.8" } @@ -5906,6 +6147,12 @@ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "16.0.5", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", @@ -6382,6 +6629,7 @@ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/spy": "2.1.5", "@vitest/utils": "2.1.5", @@ -6397,6 +6645,7 @@ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/spy": "2.1.5", "estree-walker": "^3.0.3", @@ -6419,10 +6668,11 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", - "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^1.2.0" }, @@ -6435,6 +6685,7 @@ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/utils": "2.1.5", "pathe": "^1.1.2" @@ -6448,6 +6699,7 @@ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.1.5", "magic-string": "^0.30.12", @@ -6457,11 +6709,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", + "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/spy": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^3.0.2" }, @@ -6474,6 +6740,7 @@ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/pretty-format": "2.1.5", "loupe": "^3.1.2", @@ -6483,6 +6750,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", + "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@wry/caches": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", @@ -6896,6 +7176,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -7323,39 +7604,6 @@ "node": ">=8" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -7694,6 +7942,7 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7817,6 +8066,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -7871,7 +8121,8 @@ "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" }, "node_modules/chart.js": { "version": "4.4.6", @@ -7890,6 +8141,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" } @@ -7986,17 +8238,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -8064,11 +8305,12 @@ } }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/cliui": { @@ -8787,6 +9029,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -8814,25 +9057,6 @@ "node": ">=0.10.0" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", @@ -9354,7 +9578,8 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.0.0", @@ -10129,6 +10354,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -10295,6 +10521,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -10308,6 +10535,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -10393,28 +10621,6 @@ "bser": "2.1.1" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -11008,6 +11214,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -11375,6 +11582,7 @@ "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "dev": true, + "license": "MIT", "dependencies": { "harmony-reflect": "^1.4.6" }, @@ -11509,79 +11717,22 @@ "dev": true }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz", + "integrity": "sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==", + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "@inquirer/core": "^9.2.1", + "@inquirer/prompts": "^6.0.1", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/internal-slot": { @@ -11887,14 +12038,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -12109,17 +12252,6 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -14453,7 +14585,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", @@ -14500,36 +14633,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -14689,7 +14792,8 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lower-case": { "version": "2.0.2", @@ -15041,6 +15145,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "engines": { "node": ">=6" } @@ -15141,9 +15246,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/nanoid": { "version": "3.3.7", @@ -15557,6 +15666,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -15634,66 +15744,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -15703,6 +15753,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -15924,13 +15975,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.16" } @@ -17404,9 +17457,10 @@ ] }, "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -17742,7 +17796,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/simple-swizzle": { "version": "0.2.2", @@ -18200,6 +18255,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -18348,11 +18404,6 @@ "node": ">=8" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -18450,6 +18501,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -18458,6 +18510,7 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -19298,8 +19351,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "devOptional": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -19723,6 +19775,7 @@ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", @@ -20056,6 +20109,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/expect": "2.1.5", "@vitest/mocker": "2.1.5", @@ -20168,14 +20222,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-vitals": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", @@ -20517,6 +20563,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", diff --git a/package.json b/package.json index aee85b5772..a922f88205 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.6.1", - "inquirer": "^8.0.0", + "inquirer": "^11.0.2", "js-cookie": "^3.0.1", "lcov-result-merger": "^5.0.1", "markdown-toc": "^1.2.0", diff --git a/public/locales/en/errors.json b/public/locales/en/errors.json index 752c0db750..ffb0e6f146 100644 --- a/public/locales/en/errors.json +++ b/public/locales/en/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email not registered", "notFoundMsg": "Oops! The Page you requested was not found!", "errorOccurredCouldntCreate": "An error occurred. Couldn't create {{entity}}", - "errorLoading": "Error occured while loading {{entity}} data" + "errorLoading": "Error occured while loading {{entity}} data", + "invalidPhoneNumber": "Please enter a valid phone number", + "invalidEducationGrade": "Please select a valid education grade", + "invalidEmploymentStatus": "Please select a valid employment status", + "invalidMaritalStatus": "Please select a valid marital status", + "error400": "The submitted information is invalid. Please check your inputs and try again" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5da5ef49ee..5f6d319588 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -923,7 +923,7 @@ "register": "register" }, "addOnStore": { - "title": "Add On Store", + "title": "Plugin Store", "searchName": "Ex: Donations", "search": "Search", "enable": "Enabled", @@ -1227,7 +1227,7 @@ "RstartDate": "Select Start Date", "RendDate": "Select End Date", "RClose": "Close the window", - "addNew": "Create new advertisement", + "addNew": "Create", "EXname": "Ex. Cookie Shop", "EXlink": "Ex. http://yourwebsite.com/photo", "createAdvertisement": "Create Advertisement", @@ -1405,6 +1405,9 @@ "userPledges": { "title": "My Pledges" }, + "leaveOrganization": { + "title": "Leave Organization" + }, "eventVolunteers": { "volunteers": "Volunteers", "volunteer": "Volunteer", diff --git a/public/locales/fr/errors.json b/public/locales/fr/errors.json index e9a7cf4fd9..ae53237404 100644 --- a/public/locales/fr/errors.json +++ b/public/locales/fr/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email non enregistré", "notFoundMsg": "Oops! ", "errorOccurredCouldntCreate": "Une erreur s'est produite. Impossible de créer {{entity}}", - "errorLoading": "Une erreur s'est produite lors du chargement des données {{entity}}" + "errorLoading": "Une erreur s'est produite lors du chargement des données {{entity}}", + "invalidPhoneNumber": "Veuillez entrer un numéro de téléphone valide", + "invalidEducationGrade": "Veuillez sélectionner un niveau d'études valide", + "invalidEmploymentStatus": "Veuillez sélectionner un statut d'emploi valide", + "invalidMaritalStatus": "Veuillez sélectionner un état matrimonial valide", + "error400": "Réponse non réussie. Code d'état 400 reçu du serveur" } diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index d089aefeb5..8dc5fa888a 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -1405,6 +1405,9 @@ "userPledges": { "title": "Mes Promesses" }, + "leaveOrganization": { + "title": "Quitter l'organisation" + }, "eventVolunteers": { "volunteers": "Bénévoles", "volunteer": "Bénévole", diff --git a/public/locales/hi/errors.json b/public/locales/hi/errors.json index 63b6c3f5d3..64f9523180 100644 --- a/public/locales/hi/errors.json +++ b/public/locales/hi/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "ईमेल पंजीकृत नहीं है", "notFoundMsg": "उफ़! ", "errorOccurredCouldntCreate": "एक त्रुटि हुई। {{entity}} नहीं बना सके", - "errorLoading": "{{entity}} डेटा लोड करते समय त्रुटि हुई" + "errorLoading": "{{entity}} डेटा लोड करते समय त्रुटि हुई", + "invalidPhoneNumber": "कृपया एक मान्य फोन-नंबर दर्ज करे", + "invalidEducationGrade": "कृपया एक शिक्षा ग्रेड चुनें", + "invalidEmploymentStatus": "कृपया वैध रोजगार स्थिति चुनें", + "invalidMaritalStatus": "कृपया वैध वैवाहिक स्थिति चुनें", + "error400": "आपकी जानकारी सहेजी नहीं जा सकी। कृपया अपनी प्रविष्टियों की जांच करें और पुनः प्रयास करें।" } diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 2645340b23..2865e72e1f 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -1405,6 +1405,9 @@ "userPledges": { "title": "मेरी प्रतिज्ञाएँ" }, + "leaveOrganization": { + "title": "संगठन छोड़ें" + }, "eventVolunteers": { "volunteers": "स्वयंसेवक", "volunteer": "स्वयंसेवक", diff --git a/public/locales/sp/errors.json b/public/locales/sp/errors.json index 39b579abac..7489356b5e 100644 --- a/public/locales/sp/errors.json +++ b/public/locales/sp/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email not registered", "notFoundMsg": "Oops! The Page you requested was not found!", "errorOccurredCouldntCreate": "Ocurrió un error. No se pudo crear {{entity}}", - "errorLoading": "Ocurrió un error al cargar los datos de {{entity}}" + "errorLoading": "Ocurrió un error al cargar los datos de {{entity}}", + "invalidPhoneNumber": "Por favor, introduzca un número de teléfono válido", + "invalidEducationGrade": "Por favor seleccione un grado de educación válido", + "invalidEmploymentStatus": "Por favor seleccione un estado de empleo válido", + "invalidMaritalStatus": "Por favor seleccione un estado civil válido", + "error400": "Respuesta no exitosa. Se recibió el código de estado 400 del servidor" } diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index 7aa1d6ffc0..da91efb41d 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -1407,6 +1407,9 @@ "userPledges": { "title": "Mis Promesas" }, + "leaveOrganization": { + "title": "Dejar la organización" + }, "eventVolunteers": { "volunteers": "Voluntarios", "volunteer": "Voluntario", diff --git a/public/locales/zh/errors.json b/public/locales/zh/errors.json index c872f367a5..c289d67aa1 100644 --- a/public/locales/zh/errors.json +++ b/public/locales/zh/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "邮箱未注册", "notFoundMsg": "哎呀!", "errorOccurredCouldntCreate": "发生错误。 无法创建{{entity}}", - "errorLoading": "加载{{entity}}数据时出错" + "errorLoading": "加载{{entity}}数据时出错", + "invalidPhoneNumber": "请选择一个有效的电话号码", + "invalidEducationGrade": "请选择教育年级", + "invalidEmploymentStatus": "请选择有效的就业状况", + "invalidMaritalStatus": "请选择有效的婚姻状况", + "error400": "响应不成功. 从服务器收到状态代码 400" } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index adafbdbe45..5fbbf4b870 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -1405,6 +1405,9 @@ "userPledges": { "title": "我的承诺" }, + "leaveOrganization": { + "title": "离开组织" + }, "eventVolunteers": { "volunteers": "志愿者", "volunteer": "志愿者", diff --git a/src/App.tsx b/src/App.tsx index 37f3bc301e..4d2ca76010 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,7 @@ import EventDashboardScreen from 'components/EventDashboardScreen/EventDashboard import Campaigns from 'screens/UserPortal/Campaigns/Campaigns'; import Pledges from 'screens/UserPortal/Pledges/Pledges'; import VolunteerManagement from 'screens/UserPortal/Volunteer/VolunteerManagement'; +import LeaveOrganization from 'screens/UserPortal/LeaveOrganization/LeaveOrganization'; const { setItem } = useLocalStorage(); @@ -198,6 +199,10 @@ function app(): JSX.Element { } /> } /> } /> + } + /> } diff --git a/src/GraphQl/Mutations/ActionItemCategoryMutations.ts b/src/GraphQl/Mutations/ActionItemCategoryMutations.ts index 92e7b0968c..4b4c51bc78 100644 --- a/src/GraphQl/Mutations/ActionItemCategoryMutations.ts +++ b/src/GraphQl/Mutations/ActionItemCategoryMutations.ts @@ -29,7 +29,7 @@ export const CREATE_ACTION_ITEM_CATEGORY_MUTATION = gql` * * @param id - The id of the ActionItemCategory to be updated. * @param name - Updated name of the ActionItemCategory. - * @param isDisabled - Updated disabled status of the ActionItemCategory. + * @param isDisabled - Updated disabled status of the ActionItemCategory. */ export const UPDATE_ACTION_ITEM_CATEGORY_MUTATION = gql` diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css b/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css index 1f1ea89996..c5dd86c8d4 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css @@ -7,8 +7,12 @@ margin-left: auto; display: flex !important; align-items: center; + background-color: transparent; + color: #31bb6b; +} +.card { + border: 4px solid green; } - .entryaction i { margin-right: 8px; } diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx index 257917e2c2..12805568f6 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx @@ -17,9 +17,9 @@ interface InterfaceAddOnEntryProps { description?: string; // Optional props createdBy: string; component?: string; // Optional props - modified?: any; // Optional props + modified?: boolean; // Optional props uninstalledOrgs: string[]; - getInstalledPlugins: () => any; + getInstalledPlugins: () => void; } /** @@ -59,6 +59,7 @@ function addOnEntry({ // Getting orgId from URL parameters const { orgId: currentOrg } = useParams(); + // console.log(currentOrg); if (!currentOrg) { // If orgId is not present in the URL, navigate to the org list page return ; @@ -101,7 +102,10 @@ function addOnEntry({ return ( <> - + {/* {uninstalledOrgs.includes(currentOrg) && ( )} */} - {title} + {title} {createdBy} @@ -134,7 +138,7 @@ function addOnEntry({ ) : ( )} {/* {installed ? 'Remove' : configurable ? 'Installed' : 'Install'} */} diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.module.css b/src/components/AddOn/core/AddOnStore/AddOnStore.module.css index 8a34c03be5..9f5bb6c868 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.module.css +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.module.css @@ -11,8 +11,12 @@ border-bottom: 3px solid #31bb6b; width: 15%; } - -.actioninput { +.input { + display: flex; + position: relative; + width: 560px; +} +/* .actioninput { text-decoration: none; margin-bottom: 50px; border-color: #e8e5e5; @@ -23,9 +27,46 @@ padding-right: 10px; padding-left: 10px; box-shadow: none; +} */ +.actioninput { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px #31bb6b; +} +.inputField > button { + padding-top: 10px; + padding-bottom: 10px; } .actionradio input { width: fit-content; margin: inherit; } +.cardGridItem { + width: 38vw; +} +.justifysp { + display: grid; + width: 100%; + justify-content: space-between; + align-items: baseline; + grid-template-rows: auto; + grid-template-columns: repeat(2, 1fr); + grid-gap: 0.8rem 0.4rem; +} + +@media screen and (max-width: 600px) { + .cardGridItem { + width: 100%; + } + .justifysp { + display: grid; + width: 100%; + justify-content: center; + align-items: start; + grid-template-rows: auto; + grid-template-columns: 1fr; + grid-gap: 0.8rem 0.4rem; + } +} diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx b/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx index e76e2a7b73..abb4a80ce8 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx @@ -22,7 +22,11 @@ import useLocalStorage from 'utils/useLocalstorage'; import { MockedProvider } from '@apollo/react-testing'; const { getItem } = useLocalStorage(); - +interface InterfacePlugin { + enabled: boolean; + pluginName: string; + component: string; +} jest.mock('components/AddOn/support/services/Plugin.helper', () => ({ __esModule: true, default: jest.fn().mockImplementation(() => ({ @@ -60,16 +64,18 @@ jest.mock('components/AddOn/support/services/Plugin.helper', () => ({ }, // Add more mock data as needed ]), - generateLinks: jest.fn().mockImplementation((plugins) => { - return plugins - .filter((plugin: { enabled: any }) => plugin.enabled) - .map((installedPlugin: { pluginName: any; component: string }) => { - return { - name: installedPlugin.pluginName, - url: `/plugin/${installedPlugin.component.toLowerCase()}`, - }; - }); - }), + generateLinks: jest + .fn() + .mockImplementation((plugins: InterfacePlugin[]) => { + return plugins + .filter((plugin) => plugin.enabled) + .map((installedPlugin) => { + return { + name: installedPlugin.pluginName, + url: `/plugin/${installedPlugin.component.toLowerCase()}`, + }; + }); + }), })), })); @@ -301,77 +307,6 @@ describe('Testing AddOnStore Component', () => { expect(message.length).toBeGreaterThanOrEqual(1); }); - test('check filters enabled and disabled under Installed tab', async () => { - const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_GET_MOCK]; - render( - - - - - - - - - - - , - ); - - await wait(); - userEvent.click(screen.getByText('Installed')); - - expect(screen.getByText('Filters')).toBeInTheDocument(); - expect(screen.getByLabelText('Enabled')).toBeInTheDocument(); - expect(screen.getByLabelText('Disabled')).toBeInTheDocument(); - - fireEvent.click(screen.getByLabelText('Enabled')); - expect(screen.getByLabelText('Enabled')).toBeChecked(); - fireEvent.click(screen.getByLabelText('Disabled')); - expect(screen.getByLabelText('Disabled')).toBeChecked(); - }); - - test('check the working search bar when on Installed tab', async () => { - const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_GET_MOCK]; - - const { container } = render( - - - - - - - - - - - , - ); - await wait(); - userEvent.click(screen.getByText('Installed')); - - await wait(); - let searchText = ''; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - expect(container).toHaveTextContent('Plugin 1'); - expect(container).toHaveTextContent('Plugin 3'); - - searchText = 'Plugin 1'; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - const plugin1Elements = screen.queryAllByText('Plugin 1'); - expect(plugin1Elements.length).toBeGreaterThan(1); - - searchText = 'Test Plugin'; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - const message = screen.getAllByText('Plugin does not exists'); - expect(message.length).toBeGreaterThanOrEqual(1); - }); - test('AddOnStore loading test', async () => { expect(true).toBe(true); const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_LOADING_MOCK]; diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx index 878ad64e31..90a32d9bb3 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx @@ -1,16 +1,27 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState } from 'react'; -// import PropTypes from 'react'; import styles from './AddOnStore.module.css'; import AddOnEntry from '../AddOnEntry/AddOnEntry'; -import Action from '../../support/components/Action/Action'; import { useQuery } from '@apollo/client'; -import { PLUGIN_GET } from 'GraphQl/Queries/Queries'; // GraphQL query for fetching plugins -import { Col, Form, Row, Tab, Tabs } from 'react-bootstrap'; +import { PLUGIN_GET } from 'GraphQl/Queries/Queries'; // PLUGIN_LIST +import { Col, Dropdown, Form, Row, Tab, Tabs, Button } from 'react-bootstrap'; import PluginHelper from 'components/AddOn/support/services/Plugin.helper'; import { store } from './../../../../state/store'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { Search } from '@mui/icons-material'; + +interface InterfacePluginHelper { + _id: string; + pluginName?: string; + pluginDesc?: string; + pluginCreatedBy: string; + pluginInstallStatus?: boolean; + uninstalledOrgs: string[]; + installed: boolean; + enabled: boolean; + name: string; + component: string; +} /** * Component for managing and displaying plugins in the store. @@ -30,12 +41,13 @@ function addOnStore(): JSX.Element { const [isStore, setIsStore] = useState(true); const [showEnabled, setShowEnabled] = useState(true); const [searchText, setSearchText] = useState(''); - const [, setDataList] = useState([]); + const [, setDataList] = useState([]); - // type plugData = { pluginName: String, plug }; - const { data, loading } = useQuery(PLUGIN_GET); + const { data, loading } = useQuery<{ getPlugins: InterfacePluginHelper[] }>( + PLUGIN_GET, + ); - const { orgId } = useParams(); + const { orgId } = useParams<{ orgId: string }>(); /** * Fetches store plugins and updates the Redux store with the plugin data. @@ -44,10 +56,10 @@ function addOnStore(): JSX.Element { const getStorePlugins = async (): Promise => { let plugins = await new PluginHelper().fetchStore(); const installIds = (await new PluginHelper().fetchInstalled()).map( - (plugin: any) => plugin.id, + (plugin: InterfacePluginHelper) => plugin._id, ); - plugins = plugins.map((plugin: any) => { - plugin.installed = installIds.includes(plugin.id); + plugins = plugins.map((plugin: InterfacePluginHelper) => { + plugin.installed = installIds.includes(plugin._id); return plugin; }); store.dispatch({ type: 'UPDATE_STORE', payload: plugins }); @@ -57,8 +69,8 @@ function addOnStore(): JSX.Element { * Sets the list of installed plugins in the component's state. */ /* istanbul ignore next */ - const getInstalledPlugins: () => any = () => { - setDataList(data); + const getInstalledPlugins: () => void = () => { + setDataList(data?.getPlugins ?? []); }; /** @@ -66,10 +78,14 @@ function addOnStore(): JSX.Element { * * @param tab - The key of the selected tab (either 'available' or 'installed'). */ - const updateSelectedTab = (tab: any): void => { + const updateSelectedTab = (tab: string): void => { setIsStore(tab === 'available'); /* istanbul ignore next */ - isStore ? getStorePlugins() : getInstalledPlugins(); + if (tab === 'available') { + getStorePlugins(); + } else { + getInstalledPlugins(); + } }; /** @@ -77,10 +93,23 @@ function addOnStore(): JSX.Element { * * @param ev - The event object from the filter change. */ - const filterChange = (ev: any): void => { + const filterChange = (ev: React.ChangeEvent): void => { setShowEnabled(ev.target.value === 'enabled'); }; + const filterPlugins = ( + plugins: InterfacePluginHelper[], + searchTerm: string, + ): InterfacePluginHelper[] => { + if (!searchTerm) { + return plugins; + } + + return plugins.filter((plugin) => + plugin.pluginName?.toLowerCase().includes(searchTerm.toLowerCase()), + ); + }; + // Show a loader while the data is being fetched /* istanbul ignore next */ if (loading) { @@ -93,9 +122,23 @@ function addOnStore(): JSX.Element { return ( <> - - - + + +
setSearchText(e.target.value)} /> - + +
{!isStore && ( - -
-
- - -
-
-
+ + filterChange( + e as unknown as React.ChangeEvent, + ) + } + > + + {showEnabled ? t('enable') : t('disable')} + + + + {t('enable')} + + + {t('disable')} + + + )} - -
-

{t('pHeading')}

- {searchText ? ( -

- Search results for {searchText} -

- ) : null} +
+ { + if (eventKey) { + updateSelectedTab(eventKey); + } + }} + > + +
+ {(() => { + const filteredPlugins = filterPlugins( + data?.getPlugins || [], + searchText, + ); - {t('pMessage')}; + } + + return ( +
+ {filteredPlugins.map((plug, i) => ( +
+ +
+ ))} +
+ ); + })()} +
+
+ - - {data.getPlugins.filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ).length === 0 ? ( -

{t('pMessage')}

- ) : ( - data.getPlugins - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ) - .map( - ( - plug: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - uninstalledOrgs: string[]; - getInstalledPlugins: () => any; - }, - i: React.Key | null | undefined, - ): JSX.Element => ( - - ), - ) - )} -
- - {data.getPlugins - .filter( - (plugin: any) => !plugin.uninstalledOrgs.includes(orgId), - ) - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ).length === 0 ? ( -

{t('pMessage')}

- ) : ( - data.getPlugins - .filter( - (plugin: any) => !plugin.uninstalledOrgs.includes(orgId), - ) - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ) - .map( - ( - plug: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - uninstalledOrgs: string[]; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }, - i: React.Key | null | undefined, - ): JSX.Element => ( - - ), - ) - )} -
-
-
- +
+ {(() => { + const installedPlugins = (data?.getPlugins || []).filter( + (plugin) => !plugin.uninstalledOrgs.includes(orgId ?? ''), + ); + const filteredPlugins = filterPlugins( + installedPlugins, + searchText, + ); + + if (filteredPlugins.length === 0) { + return

{t('pMessage')}

; + } + + return filteredPlugins.map((plug, i) => ( +
+ +
+ )); + })()} +
+ + +
); diff --git a/src/components/AddOn/support/components/Action/Action.spec.tsx b/src/components/AddOn/support/components/Action/Action.spec.tsx new file mode 100644 index 0000000000..e0682a5645 --- /dev/null +++ b/src/components/AddOn/support/components/Action/Action.spec.tsx @@ -0,0 +1,31 @@ +/** + * Unit tests for the Action component. + * + * This file contains tests for the Action component to ensure it behaves as expected + * under various scenarios. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { describe, test, expect } from 'vitest'; + +import { store } from 'state/store'; +import Action from './Action'; + +describe('Testing Action Component', () => { + const props = { + children: 'dummy children', + label: 'dummy label', + }; + + test('should render props and text elements for the page component', () => { + const { getByText } = render( + + + , + ); + + expect(getByText(props.label)).toBeInTheDocument(); + expect(getByText(props.children)).toBeInTheDocument(); + }); +}); diff --git a/src/components/AddOn/support/components/SidePanel/SidePanel.test.tsx b/src/components/AddOn/support/components/SidePanel/SidePanel.spec.tsx similarity index 82% rename from src/components/AddOn/support/components/SidePanel/SidePanel.test.tsx rename to src/components/AddOn/support/components/SidePanel/SidePanel.spec.tsx index d929278d0e..4a5f4e5692 100644 --- a/src/components/AddOn/support/components/SidePanel/SidePanel.test.tsx +++ b/src/components/AddOn/support/components/SidePanel/SidePanel.spec.tsx @@ -11,11 +11,16 @@ const client: ApolloClient = new ApolloClient({ }); describe('Testing Contribution Stats', () => { + /** + * Props to be passed to the `SidePanel` component during the test. + */ const props = { collapse: true, children: '234', }; - + /** + * Verifies that the `SidePanel` component renders correctly with given props. + */ test('should render props and text elements test for the SidePanel component', () => { render( diff --git a/src/components/AddOn/support/services/Plugin.helper.test.ts b/src/components/AddOn/support/services/Plugin.helper.spec.ts similarity index 53% rename from src/components/AddOn/support/services/Plugin.helper.test.ts rename to src/components/AddOn/support/services/Plugin.helper.spec.ts index e024734247..51c8ec4bc5 100644 --- a/src/components/AddOn/support/services/Plugin.helper.test.ts +++ b/src/components/AddOn/support/services/Plugin.helper.spec.ts @@ -1,15 +1,39 @@ import PluginHelper from './Plugin.helper'; +import { vi } from 'vitest'; + +/** + * This file contains unit tests for the PluginHelper component. + * + * The tests cover: + * - Verification that the class contains the required method definitions. + * - Correct functionality of the `generateLinks` method, including returning proper objects. + * - Proper behavior of the `fetchStore` method, including handling of mocked JSON responses. + * - Functionality of the `fetchInstalled` method, verifying it returns the expected JSON data. + * + * These tests use Vitest for test execution and mock the global `fetch` function for asynchronous tests. + */ describe('Testing src/components/AddOn/support/services/Plugin.helper.ts', () => { - test('Class should contain the required method definitions', () => { + it('Class should contain the required method definitions', () => { const pluginHelper = new PluginHelper(); expect(pluginHelper).toHaveProperty('fetchStore'); expect(pluginHelper).toHaveProperty('fetchInstalled'); expect(pluginHelper).toHaveProperty('generateLinks'); expect(pluginHelper).toHaveProperty('generateLinks'); }); - test('generateLinks should return proper objects', () => { - const obj = { enabled: true, name: 'demo', component: 'samplecomponent' }; + it('generateLinks should return proper objects', () => { + const obj = { + enabled: true, + name: 'demo', + component: 'samplecomponent', + _id: 'someId', + pluginName: 'pluginName', + pluginDesc: 'pluginDesc', + pluginCreatedBy: 'creator', + pluginInstallStatus: true, + uninstalledOrgs: ['org1', 'org2'], + installed: true, + }; const objToMatch = { name: 'demo', url: '/plugin/samplecomponent' }; const pluginHelper = new PluginHelper(); const val = pluginHelper.generateLinks([obj]); @@ -17,9 +41,9 @@ describe('Testing src/components/AddOn/support/services/Plugin.helper.ts', () => }); it('fetchStore should return expected JSON', async () => { const helper = new PluginHelper(); - const spy = jest.spyOn(global, 'fetch').mockImplementation(() => { + const spy = vi.spyOn(global, 'fetch').mockImplementation(() => { const response = new Response(); - response.json = jest + response.json = vi .fn() .mockReturnValue(Promise.resolve({ data: 'mock data' })); return Promise.resolve(response); @@ -35,11 +59,11 @@ describe('Testing src/components/AddOn/support/services/Plugin.helper.ts', () => { name: 'plugin1', component: 'Component1', enabled: true }, { name: 'plugin2', component: 'Component2', enabled: false }, ]; - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { const response = new Response(); - response.json = jest.fn().mockReturnValue(Promise.resolve(mockResponse)); + response.json = vi.fn().mockReturnValue(Promise.resolve(mockResponse)); return Promise.resolve(response); - }) as jest.Mock; + }); const result = await pluginHelper.fetchInstalled(); expect(result).toEqual(mockResponse); }); diff --git a/src/components/Advertisements/Advertisements.module.css b/src/components/Advertisements/Advertisements.module.css index 8a34c03be5..6d9eb7f612 100644 --- a/src/components/Advertisements/Advertisements.module.css +++ b/src/components/Advertisements/Advertisements.module.css @@ -1,6 +1,13 @@ .container { display: flex; } +.listBox { + display: grid; + width: 100%; + grid-template-rows: auto; + grid-template-columns: repeat(6, 1fr); + grid-gap: 0.8rem 0.4rem; +} .logintitle { color: #707070; @@ -11,15 +18,24 @@ border-bottom: 3px solid #31bb6b; width: 15%; } - +.input { + display: flex; + position: relative; + width: 560px; +} +.justifysp { + display: grid; + width: 100%; + margin-top: 30px; +} .actioninput { text-decoration: none; - margin-bottom: 50px; + /* margin-bottom: 50px; */ border-color: #e8e5e5; - width: 80%; + background-color: white; border-radius: 7px; - padding-top: 5px; - padding-bottom: 5px; + padding-top: 10px; + padding-bottom: 10px; padding-right: 10px; padding-left: 10px; box-shadow: none; diff --git a/src/components/Advertisements/Advertisements.test.tsx b/src/components/Advertisements/Advertisements.test.tsx index c0992a1012..88bbb1255c 100644 --- a/src/components/Advertisements/Advertisements.test.tsx +++ b/src/components/Advertisements/Advertisements.test.tsx @@ -461,7 +461,7 @@ describe('Testing Advertisement Component', () => { await wait(); const date = await screen.findAllByTestId('Ad_end_date'); - const dateString = date[1].innerHTML; + const dateString = date[0].innerHTML; const dateMatch = dateString.match( /\b(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})\s+(\d{4})\b/, ); diff --git a/src/components/Advertisements/Advertisements.tsx b/src/components/Advertisements/Advertisements.tsx index f20c2a7d8e..5f0e2b2033 100644 --- a/src/components/Advertisements/Advertisements.tsx +++ b/src/components/Advertisements/Advertisements.tsx @@ -2,30 +2,16 @@ import React, { useEffect, useState } from 'react'; import styles from './Advertisements.module.css'; import { useQuery } from '@apollo/client'; import { ORGANIZATION_ADVERTISEMENT_LIST } from 'GraphQl/Queries/Queries'; -import { Col, Row, Tab, Tabs } from 'react-bootstrap'; +import { Button, Col, Form, Row, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import AdvertisementEntry from './core/AdvertisementEntry/AdvertisementEntry'; import AdvertisementRegister from './core/AdvertisementRegister/AdvertisementRegister'; import { useParams } from 'react-router-dom'; import type { InterfaceQueryOrganizationAdvertisementListItem } from 'utils/interfaces'; import InfiniteScroll from 'react-infinite-scroll-component'; +import { Search } from '@mui/icons-material'; -/** - * The `Advertisements` component displays a list of advertisements for a specific organization. - * It uses a tab-based interface to toggle between active and archived advertisements. - * - * The component utilizes the `useQuery` hook from Apollo Client to fetch advertisements data - * and implements infinite scrolling to load more advertisements as the user scrolls. - * - * @example - * return ( - * - * ) - * - */ - -export default function Advertisements(): JSX.Element { - // Retrieve the organization ID from URL parameters +export default function advertisements(): JSX.Element { const { orgId: currentOrgId } = useParams(); // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); @@ -43,20 +29,14 @@ export default function Advertisements(): JSX.Element { name: string; type: 'BANNER' | 'MENU' | 'POPUP'; mediaUrl: string; - endDate: string; // Assuming it's a string in the format 'yyyy-MM-dd' - startDate: string; // Assuming it's a string in the format 'yyyy-MM-dd' + endDate: string; + startDate: string; }; // GraphQL query to fetch the list of advertisements - const { - data: orgAdvertisementListData, - refetch, - }: { - data?: { - organizations: InterfaceQueryOrganizationAdvertisementListItem[]; - }; - refetch: () => void; - } = useQuery(ORGANIZATION_ADVERTISEMENT_LIST, { + const { data: orgAdvertisementListData, refetch } = useQuery<{ + organizations: InterfaceQueryOrganizationAdvertisementListItem[]; + }>(ORGANIZATION_ADVERTISEMENT_LIST, { variables: { id: currentOrgId, after: after, @@ -99,19 +79,45 @@ export default function Advertisements(): JSX.Element { return ( <> - +
- {/* Component for registering a new advertisement */} - + +
+ setSearchText("search")} + /> + +
+ + - {/* Tabs for active and archived advertisements */} - {/* Tab for active advertisements */} - + - - {/* Tab for archived advertisements */} new Date(ad.endDate) < new Date(), ).length !== 0 && (
-
{tCommon('endOfResults')}
+
{t('endOfResults')}
) } diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css index 879d96a0a0..e4f244807f 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css @@ -20,7 +20,7 @@ .admedia { object-fit: cover; - height: 20rem; + height: 16rem; } .buttons { @@ -28,6 +28,10 @@ justify-content: flex-end; } +.card { + width: 28rem; +} + .dropdownButton { background-color: transparent; color: #000; diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.spec.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.spec.tsx new file mode 100644 index 0000000000..4d27df6e22 --- /dev/null +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.spec.tsx @@ -0,0 +1,649 @@ +import React from 'react'; +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import { + ApolloClient, + ApolloProvider, + InMemoryCache, + ApolloLink, + HttpLink, +} from '@apollo/client'; +import type { NormalizedCacheObject } from '@apollo/client'; +import { BrowserRouter } from 'react-router-dom'; +import AdvertisementEntry from './AdvertisementEntry'; +import AdvertisementRegister from '../AdvertisementRegister/AdvertisementRegister'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import { BACKEND_URL } from 'Constant/constant'; +import i18nForTest from 'utils/i18nForTest'; +import { I18nextProvider } from 'react-i18next'; +import dayjs from 'dayjs'; +import useLocalStorage from 'utils/useLocalstorage'; +import { MockedProvider } from '@apollo/client/testing'; +import { ORGANIZATION_ADVERTISEMENT_LIST } from 'GraphQl/Queries/OrganizationQueries'; +import { DELETE_ADVERTISEMENT_BY_ID } from 'GraphQl/Mutations/mutations'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import '@testing-library/jest-dom'; + +const { getItem } = useLocalStorage(); + +const httpLink = new HttpLink({ + uri: BACKEND_URL, + headers: { + authorization: 'Bearer ' + getItem('token') || '', + }, +}); +const translations = JSON.parse( + JSON.stringify( + i18nForTest.getDataByLanguage('en')?.translation?.advertisement ?? null, + ), +); + +const client: ApolloClient = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.from([httpLink]), +}); + +const mockUseMutation = vi.fn(); +vi.mock('@apollo/client', async () => { + const actual = await vi.importActual('@apollo/client'); + return { + ...actual, + useMutation: () => mockUseMutation(), + }; +}); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: '1' }), + }; +}); + +describe('Testing Advertisement Entry Component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('Testing rendering and deleting of advertisement', async () => { + const deleteAdByIdMock = vi.fn(); + mockUseMutation.mockReturnValue([deleteAdByIdMock]); + const { getByTestId, getAllByText } = render( + + + + + + + + + , + ); + + //Testing rendering + expect(getByTestId('AdEntry')).toBeInTheDocument(); + expect(getAllByText('POPUP')[0]).toBeInTheDocument(); + expect(getAllByText('Advert1')[0]).toBeInTheDocument(); + expect(screen.getByTestId('media')).toBeInTheDocument(); + + //Testing successful deletion + fireEvent.click(getByTestId('moreiconbtn')); + fireEvent.click(getByTestId('deletebtn')); + + await waitFor(() => { + expect(screen.getByTestId('delete_title')).toBeInTheDocument(); + expect(screen.getByTestId('delete_body')).toBeInTheDocument(); + }); + + fireEvent.click(getByTestId('delete_yes')); + + await waitFor(() => { + expect(deleteAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + }, + }); + const deletedMessage = screen.queryByText('Advertisement Deleted'); + expect(deletedMessage).toBeNull(); + }); + + //Testing unsuccessful deletion + deleteAdByIdMock.mockRejectedValueOnce(new Error('Deletion Failed')); + + fireEvent.click(getByTestId('moreiconbtn')); + + fireEvent.click(getByTestId('delete_yes')); + + await waitFor(() => { + expect(deleteAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + }, + }); + const deletionFailedText = screen.queryByText((content, element) => { + return ( + element?.textContent === 'Deletion Failed' && + element.tagName.toLowerCase() === 'div' + ); + }); + expect(deletionFailedText).toBeNull(); + }); + }); + + it('should use default props when none are provided', () => { + render( + , + : void { + throw new Error('Function not implemented.'); + }} + />, + ); + + //Check if component renders with default ''(empty string) + const elements = screen.getAllByText(''); // This will return an array of matching elements + elements.forEach((element) => expect(element).toBeInTheDocument()); + + // Check that the component renders with default `mediaUrl` (empty string) + const mediaElement = screen.getByTestId('media'); + expect(mediaElement).toHaveAttribute('src', ''); + + // Check that the component renders with default `endDate` + const defaultEndDate = new Date().toDateString(); + expect(screen.getByText(`Ends on ${defaultEndDate}`)).toBeInTheDocument(); + + // Check that the component renders with default `startDate` + const defaultStartDate = new Date().toDateString(); + expect(screen.getByText(`Ends on ${defaultStartDate}`)).toBeInTheDocument(); //fix text "Ends on"? + }); + + it('should correctly override default props when values are provided', () => { + const mockName = 'Test Ad'; + const mockType = 'Banner'; + const mockMediaUrl = 'https://example.com/media.png'; + const mockEndDate = new Date(2025, 11, 31); + const mockStartDate = new Date(2024, 0, 1); + const mockOrganizationId = 'org123'; + + const { getByText } = render( + , + : void { + throw new Error('Function not implemented.'); + }} + />, + ); + + // Check that the component renders with provided values + expect(getByText(mockName)).toBeInTheDocument(); + expect(getByText(mockType)).toBeInTheDocument(); + expect(screen.getByTestId('media')).toHaveAttribute('src', mockMediaUrl); + expect( + getByText(`Ends on ${mockEndDate.toDateString()}`), + ).toBeInTheDocument(); + }); + + it('should open and close the dropdown when options button is clicked', () => { + const { getByTestId, queryByText, getAllByText } = render( + + + + + + + + + , + ); + + // Test initial rendering + expect(getByTestId('AdEntry')).toBeInTheDocument(); + expect(getAllByText('POPUP')[0]).toBeInTheDocument(); + expect(getAllByText('Advert1')[0]).toBeInTheDocument(); + + // Test dropdown functionality + const optionsButton = getByTestId('moreiconbtn'); + + // Initially, the dropdown should not be visible + expect(queryByText('Edit')).toBeNull(); + + // Click to open the dropdown + fireEvent.click(optionsButton); + + // After clicking the button, the dropdown should be visible + expect(queryByText('Edit')).toBeInTheDocument(); + + // Click again to close the dropdown + fireEvent.click(optionsButton); + + // After the second click, the dropdown should be hidden again + expect(queryByText('Edit')).toBeNull(); + }); + + it('Updates the advertisement and shows success toast on successful update', async () => { + const updateAdByIdMock = vi.fn().mockResolvedValue({ + data: { + updateAdvertisement: { + advertisement: { + _id: '1', + name: 'Updated Advertisement', + mediaUrl: '', + startDate: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), + endDate: dayjs(new Date()).add(2, 'days').format('YYYY-MM-DD'), + type: 'BANNER', + }, + }, + }, + }); + + mockUseMutation.mockReturnValue([updateAdByIdMock]); + + render( + + + + + + + + + , + ); + + const optionsButton = screen.getByTestId('moreiconbtn'); + fireEvent.click(optionsButton); + fireEvent.click(screen.getByTestId('editBtn')); + + fireEvent.change(screen.getByLabelText('Enter name of Advertisement'), { + target: { value: 'Updated Advertisement' }, + }); + + expect(screen.getByLabelText('Enter name of Advertisement')).toHaveValue( + 'Updated Advertisement', + ); + + fireEvent.change(screen.getByLabelText(translations.Rtype), { + target: { value: 'BANNER' }, + }); + expect(screen.getByLabelText(translations.Rtype)).toHaveValue('BANNER'); + + fireEvent.change(screen.getByLabelText(translations.RstartDate), { + target: { value: dayjs().add(1, 'day').format('YYYY-MM-DD') }, + }); + + fireEvent.change(screen.getByLabelText(translations.RendDate), { + target: { value: dayjs().add(2, 'days').format('YYYY-MM-DD') }, + }); + + fireEvent.click(screen.getByTestId('addonupdate')); + + expect(updateAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + name: 'Updated Advertisement', + type: 'BANNER', + startDate: dayjs().add(1, 'day').format('YYYY-MM-DD'), + endDate: dayjs().add(2, 'days').format('YYYY-MM-DD'), + }, + }); + }); + + it('Simulating if the mutation doesnt have data variable while updating', async () => { + const updateAdByIdMock = vi.fn().mockResolvedValue({ + updateAdvertisement: { + _id: '1', + name: 'Updated Advertisement', + type: 'BANNER', + }, + }); + + mockUseMutation.mockReturnValue([updateAdByIdMock]); + + render( + + + + + + + + + , + ); + + const optionsButton = screen.getByTestId('moreiconbtn'); + fireEvent.click(optionsButton); + fireEvent.click(screen.getByTestId('editBtn')); + + fireEvent.change(screen.getByLabelText('Enter name of Advertisement'), { + target: { value: 'Updated Advertisement' }, + }); + + expect(screen.getByLabelText('Enter name of Advertisement')).toHaveValue( + 'Updated Advertisement', + ); + + fireEvent.change(screen.getByLabelText(translations.Rtype), { + target: { value: 'BANNER' }, + }); + expect(screen.getByLabelText(translations.Rtype)).toHaveValue('BANNER'); + + fireEvent.click(screen.getByTestId('addonupdate')); + + expect(updateAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + name: 'Updated Advertisement', + type: 'BANNER', + }, + }); + }); + + it('Simulating if the mutation does not have data variable while registering', async () => { + vi.stubGlobal('location', { + reload: vi.fn(), + href: 'https://example.com/page/id=1', + }); + + const createAdByIdMock = vi.fn().mockResolvedValue({ + data1: { + createAdvertisement: { + _id: '1', + }, + }, + }); + + mockUseMutation.mockReturnValue([createAdByIdMock]); + + render( + + + + + { + + } + + + + , + ); + + fireEvent.click(screen.getByTestId('createAdvertisement')); + + fireEvent.change(screen.getByLabelText('Enter name of Advertisement'), { + target: { value: 'Updated Advertisement' }, + }); + + expect(screen.getByLabelText('Enter name of Advertisement')).toHaveValue( + 'Updated Advertisement', + ); + + fireEvent.change(screen.getByLabelText(translations.Rtype), { + target: { value: 'BANNER' }, + }); + expect(screen.getByLabelText(translations.Rtype)).toHaveValue('BANNER'); + + fireEvent.change(screen.getByLabelText(translations.RstartDate), { + target: { value: '2023-01-01' }, + }); + expect(screen.getByLabelText(translations.RstartDate)).toHaveValue( + '2023-01-01', + ); + + fireEvent.change(screen.getByLabelText(translations.RendDate), { + target: { value: '2023-02-01' }, + }); + expect(screen.getByLabelText(translations.RendDate)).toHaveValue( + '2023-02-01', + ); + + fireEvent.click(screen.getByTestId('addonregister')); + + expect(createAdByIdMock).toHaveBeenCalledWith({ + variables: { + organizationId: '1', + name: 'Updated Advertisement', + file: '', + type: 'BANNER', + startDate: dayjs(new Date('2023-01-01')).format('YYYY-MM-DD'), + endDate: dayjs(new Date('2023-02-01')).format('YYYY-MM-DD'), + }, + }); + }); + + it('delete advertisement', async () => { + const deleteAdByIdMock = vi.fn(); + const mocks = [ + { + request: { + query: ORGANIZATION_ADVERTISEMENT_LIST, + variables: { + id: '1', + first: 2, + after: null, + last: null, + before: null, + }, + }, + result: { + data: { + organizations: [ + { + _id: '1', + advertisements: { + edges: [ + { + node: { + _id: '1', + name: 'Advertisement1', + startDate: '2022-01-01', + endDate: '2023-01-01', + mediaUrl: 'http://example1.com', + }, + cursor: 'cursor1', + }, + { + node: { + _id: '2', + name: 'Advertisement2', + startDate: '2024-02-01', + endDate: '2025-02-01', + mediaUrl: 'http://example2.com', + }, + cursor: 'cursor2', + }, + { + node: { + _id: '3', + name: 'Advertisement1', + startDate: '2022-01-01', + endDate: '2023-01-01', + mediaUrl: 'http://example1.com', + }, + cursor: 'cursor3', + }, + { + node: { + _id: '4', + name: 'Advertisement2', + startDate: '2024-02-01', + endDate: '2025-02-01', + mediaUrl: 'http://example2.com', + }, + cursor: 'cursor4', + }, + { + node: { + _id: '5', + name: 'Advertisement1', + startDate: '2022-01-01', + endDate: '2023-01-01', + mediaUrl: 'http://example1.com', + }, + cursor: 'cursor5', + }, + { + node: { + _id: '6', + name: 'Advertisement2', + startDate: '2024-02-01', + endDate: '2025-02-01', + mediaUrl: 'http://example2.com', + }, + cursor: 'cursor6', + }, + ], + pageInfo: { + startCursor: 'cursor1', + endCursor: 'cursor6', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 8, + }, + }, + ], + }, + }, + }, + { + request: { + query: DELETE_ADVERTISEMENT_BY_ID, + variables: { + id: '1', + }, + }, + result: { + data: { + advertisements: { + _id: null, + }, + }, + }, + }, + ]; + mockUseMutation.mockReturnValue([deleteAdByIdMock]); + const { getByTestId, getAllByText } = render( + + + + + + + + + + + , + ); + + //Testing rendering + expect(getByTestId('AdEntry')).toBeInTheDocument(); + expect(getAllByText('POPUP')[0]).toBeInTheDocument(); + expect(getAllByText('Advert1')[0]).toBeInTheDocument(); + expect(screen.getByTestId('media')).toBeInTheDocument(); + + //Testing successful deletion + fireEvent.click(getByTestId('moreiconbtn')); + fireEvent.click(getByTestId('deletebtn')); + + await waitFor(() => { + expect(screen.getByTestId('delete_title')).toBeInTheDocument(); + expect(screen.getByTestId('delete_body')).toBeInTheDocument(); + }); + + fireEvent.click(getByTestId('delete_yes')); + + await waitFor(() => { + expect(deleteAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + }, + }); + const deletedMessage = screen.queryByText('Advertisement Deleted'); + expect(deletedMessage).toBeNull(); + }); + + //Testing unsuccessful deletion + deleteAdByIdMock.mockRejectedValueOnce(new Error('Deletion Failed')); + + fireEvent.click(getByTestId('moreiconbtn')); + + fireEvent.click(getByTestId('delete_yes')); + + await waitFor(() => { + expect(deleteAdByIdMock).toHaveBeenCalledWith({ + variables: { + id: '1', + }, + }); + const deletionFailedText = screen.queryByText((content, element) => { + return ( + element?.textContent === 'Deletion Failed' && + element.tagName.toLowerCase() === 'div' + ); + }); + expect(deletionFailedText).toBeNull(); + }); + }); +}); diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx index 7368ded68e..7656f1f0cf 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx @@ -38,6 +38,7 @@ function AdvertisementEntry({ startDate = new Date(), setAfter, }: InterfaceAddOnEntryProps): JSX.Element { + console.log(id, type); const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); const { t: tCommon } = useTranslation('common'); @@ -98,7 +99,7 @@ function AdvertisementEntry({ {Array.from({ length: 1 }).map((_, idx) => ( - +
)}
- {hours.map((hour, index) => { - const timeEventsList: JSX.Element[] = - events - ?.filter((datas) => { - const currDate = new Date( - currentYear, - currentMonth, - currentDate, - ); - - if ( - parseInt(datas.startTime?.slice(0, 2) as string).toString() == - (index % 24).toString() && - datas.startDate == dayjs(currDate).format('YYYY-MM-DD') - ) { - return datas; - } - }) - .map((datas: InterfaceEventListCardProps) => { - const attendees: { _id: string }[] = []; - datas.attendees?.forEach((attendee: { _id: string }) => { - const r = { - _id: attendee._id, - }; - - attendees.push(r); - }); - - return ( - - ); - }) || []; - /*istanbul ignore next*/ - return ( -
-
-

{`${hour}`}

+ +
+
+

Holidays

+
    + {filteredHolidays.map((holiday, index) => ( +
  • + + {months[parseInt(holiday.date.slice(0, 2), 10) - 1]}{' '} + {holiday.date.slice(3)} + + {holiday.name} +
  • + ))} +
+
+ +
+

Events

+
+
+ + Holidays
-
-
0 - ? styles.event_list_parent_current - : styles.event_list_parent - } - > - {index % 24 == new Date().getHours() && - new Date().getDate() == currentDate && ( - - )} -
-
- {/*istanbul ignore next*/} - {expanded === index - ? timeEventsList - : timeEventsList?.slice(0, 1)} -
- {(timeEventsList?.length > 1 || - (windowWidth <= 700 && timeEventsList?.length > 0)) && ( - - )} -
+
+ + + Events Created by Organization + +
+
+ + + Events Created by User +
- ); - })} +
+
); }; @@ -457,22 +407,21 @@ const Calendar: React.FC = ({ return days.map((date, index) => { const className = [ date.getDay() === 0 || date.getDay() === 6 ? styles.day_weekends : '', - date.toLocaleDateString() === today.toLocaleDateString() //Styling for today day cell + date.toLocaleDateString() === today.toLocaleDateString() ? styles.day__today : '', - date.getMonth() !== currentMonth ? styles.day__outside : '', //Styling for days outside the current month + date.getMonth() !== currentMonth ? styles.day__outside : '', selectedDate?.getTime() === date.getTime() ? styles.day__selected : '', styles.day, ].join(' '); const toggleExpand = (index: number): void => { - /*istanbul ignore next*/ if (expanded === index) { setExpanded(-1); } else { setExpanded(index); } }; - /*istanbul ignore next*/ + const allEventsList: JSX.Element[] = events ?.filter((datas) => { @@ -514,13 +463,12 @@ const Calendar: React.FC = ({ ); }) || []; - const holidayList: JSX.Element[] = holidays - .filter((holiday) => { - if (holiday.date == dayjs(date).format('MM-DD')) return holiday; - }) + const holidayList: JSX.Element[] = filteredHolidays + .filter((holiday) => holiday.date === dayjs(date).format('MM-DD')) .map((holiday) => { return ; }); + return (
= ({ >
{holidayList}
- { - /*istanbul ignore next*/ - expanded === index - ? allEventsList - : holidayList?.length > 0 - ? /*istanbul ignore next*/ - allEventsList?.slice(0, 1) - : allEventsList?.slice(0, 2) - } + {expanded === index + ? allEventsList + : holidayList?.length > 0 + ? allEventsList?.slice(0, 1) + : allEventsList?.slice(0, 2)}
{(allEventsList?.length > 2 || (windowWidth <= 700 && allEventsList?.length > 0)) && ( - /*istanbul ignore next*/ )}
@@ -622,7 +560,7 @@ const Calendar: React.FC = ({ )}
{viewType == ViewType.MONTH ? ( -
+ <>
{weekdays.map((weekday, index) => (
@@ -631,18 +569,14 @@ const Calendar: React.FC = ({ ))}
{renderDays()}
-
+ + ) : viewType == ViewType.YEAR ? ( + ) : ( - // -
- {viewType == ViewType.YEAR ? ( - - ) : ( -
{renderHours()}
- )} -
+
{renderHours()}
)}
+
{viewType == ViewType.YEAR ? ( diff --git a/src/components/EventCalendar/YearlyEventCalender.tsx b/src/components/EventCalendar/YearlyEventCalender.tsx index 63870ded3c..facf75038c 100644 --- a/src/components/EventCalendar/YearlyEventCalender.tsx +++ b/src/components/EventCalendar/YearlyEventCalender.tsx @@ -46,11 +46,11 @@ interface InterfaceCalendarProps { viewType?: ViewType; } -enum Status { - ACTIVE = 'ACTIVE', - BLOCKED = 'BLOCKED', - DELETED = 'DELETED', -} +// enum Status { +// ACTIVE = 'ACTIVE', +// BLOCKED = 'BLOCKED', +// DELETED = 'DELETED', +// } /** * Enum for different user roles. @@ -63,13 +63,13 @@ enum Role { /** * Interface for event attendees. - */ -interface InterfaceIEventAttendees { - userId: string; - user?: string; - status?: Status; - createdAt?: Date; -} +// */ +// interface InterfaceIEventAttendees { +// userId: string; +// user?: string; +// status?: Status; +// createdAt?: Date; +// } /** * Interface for organization list. @@ -177,7 +177,6 @@ const Calendar: React.FC = ({ * Navigates to the previous year. */ const handlePrevYear = (): void => { - /*istanbul ignore next*/ setCurrentYear(currentYear - 1); }; @@ -185,7 +184,6 @@ const Calendar: React.FC = ({ * Navigates to the next year. */ const handleNextYear = (): void => { - /*istanbul ignore next*/ setCurrentYear(currentYear + 1); }; @@ -239,7 +237,6 @@ const Calendar: React.FC = ({ return dayjs(event.startDate).isSame(date, 'day'); }); - /*istanbul ignore next*/ const renderedEvents = eventsForCurrentDate?.map((datas: InterfaceEventListCardProps) => { const attendees: { _id: string }[] = []; @@ -276,7 +273,6 @@ const Calendar: React.FC = ({ ); }) || []; - /*istanbul ignore next*/ const toggleExpand = (index: string): void => { if (expandedY === index) { setExpandedY(null); @@ -285,7 +281,6 @@ const Calendar: React.FC = ({ } }; - /*istanbul ignore next*/ return (
{ switch (eventKey) { case 'trends': diff --git a/src/components/IconComponent/IconComponent.tsx b/src/components/IconComponent/IconComponent.tsx index 8430aca131..dd104c0408 100644 --- a/src/components/IconComponent/IconComponent.tsx +++ b/src/components/IconComponent/IconComponent.tsx @@ -19,6 +19,7 @@ import PostsIcon from 'assets/svgs/posts.svg?react'; import SettingsIcon from 'assets/svgs/settings.svg?react'; import VenueIcon from 'assets/svgs/venues.svg?react'; import RequestsIcon from 'assets/svgs/requests.svg?react'; +import ExitToAppIcon from '@mui/icons-material/ExitToApp'; import { MdOutlineVolunteerActivism } from 'react-icons/md'; import React from 'react'; @@ -134,6 +135,13 @@ const iconComponent = (props: InterfaceIconComponent): JSX.Element => { stroke={props.fill} /> ); + case 'Leave Organization': + return ( + + ); case 'Volunteer': return ( ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +const mocks = [ + { + request: { + query: EVENT_DETAILS, + variables: { id: 'event123' }, + }, + result: { + data: { + event: { + _id: 'event123', + title: 'Test Event', + description: 'This is a test event description', + startDate: '2023-05-01', + endDate: '2023-05-02', + startTime: '09:00:00', + endTime: '17:00:00', + allDay: false, + location: 'Test Location', + recurring: true, + baseRecurringEvent: { + _id: 'recurringEvent123', + }, + organization: { + _id: 'org456', + members: [ + { _id: 'member1', firstName: 'John', lastName: 'Doe' }, + { _id: 'member2', firstName: 'Jane', lastName: 'Smith' }, + ], + }, + attendees: [{ _id: 'user1' }, { _id: 'user2' }], + }, + }, + }, + }, +]; + +describe('CustomTableCell', () => { + it('renders event details correctly', async () => { + render( + + + + + + +
+
+
, + ); + + await waitFor(() => screen.getByTestId('custom-row')); + + expect(screen.getByText('Test Event')).toBeInTheDocument(); + expect( + screen.getByText( + new Date('2023-05-01').toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }), + ), + ).toBeInTheDocument(); + expect(screen.getByText('Yes')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + + const link = screen.getByRole('link', { name: 'Test Event' }); + expect(link).toHaveAttribute('href', '/event/org456/event123'); + }); + + it('displays loading state', () => { + render( + + + + + +
+
, + ); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + it('displays error state', async () => { + const errorMock = [ + { + request: { + query: EVENT_DETAILS, + variables: { id: 'event123' }, + }, + error: new Error('An error occurred'), + }, + ]; + + render( + + + + + +
+
, + ); + + await waitFor(() => { + expect( + screen.getByText( + 'Unable to load event details. Please try again later.', + ), + ).toBeInTheDocument(); + }); + }); + + it('displays no event found message', async () => { + const noEventMock = [ + { + request: { + query: EVENT_DETAILS, + variables: { id: 'event123' }, + }, + result: { + data: { + event: null, + }, + }, + }, + ]; + + render( + + + + + +
+
, + ); + + await waitFor(() => { + expect( + screen.getByText('Event not found or has been deleted'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/MemberDetail/customTableCell.test.tsx b/src/components/MemberDetail/customTableCell.test.tsx deleted file mode 100644 index bc296a74f3..0000000000 --- a/src/components/MemberDetail/customTableCell.test.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { MockedProvider } from '@apollo/client/testing'; -import { BrowserRouter } from 'react-router-dom'; -import { CustomTableCell } from './customTableCell'; -import { EVENT_DETAILS } from 'GraphQl/Queries/Queries'; - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - error: jest.fn(), - }, -})); - -const mocks = [ - { - request: { - query: EVENT_DETAILS, - variables: { id: 'event123' }, - }, - result: { - data: { - event: { - _id: 'event123', - title: 'Test Event', - description: 'This is a test event description', - startDate: '2023-05-01', - endDate: '2023-05-02', - startTime: '09:00:00', - endTime: '17:00:00', - allDay: false, - location: 'Test Location', - recurring: true, - baseRecurringEvent: { - _id: 'recurringEvent123', - }, - organization: { - _id: 'org456', - members: [ - { _id: 'member1', firstName: 'John', lastName: 'Doe' }, - { _id: 'member2', firstName: 'Jane', lastName: 'Smith' }, - ], - }, - attendees: [{ _id: 'user1' }, { _id: 'user2' }], - }, - }, - }, - }, -]; - -describe('CustomTableCell', () => { - it('renders event details correctly', async () => { - render( - - - - - - -
-
-
, - ); - - await waitFor(() => screen.getByTestId('custom-row')); - - expect(screen.getByText('Test Event')).toBeInTheDocument(); - expect(screen.getByText('May 1, 2023')).toBeInTheDocument(); - expect(screen.getByText('Yes')).toBeInTheDocument(); - expect(screen.getByText('2')).toBeInTheDocument(); - - const link = screen.getByRole('link', { name: 'Test Event' }); - expect(link).toHaveAttribute('href', '/event/org456/event123'); - }); - - it('displays loading state', () => { - render( - - - - - -
-
, - ); - - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - - // it('displays error state', async () => { - // const errorMock = [ - // { - // request: { - // query: EVENT_DETAILS, - // variables: { id: 'event123' }, - // }, - // error: new Error('An error occurred'), - // }, - // ]; - - // render( - // - // - // - // - // - //
- //
, - // ); - - // await waitFor( - // () => { - // expect( - // screen.getByText('Error loading event details'), - // ).toBeInTheDocument(); - // }, - // { timeout: 2000 }, - // ); - - // // Check if the error message from toast has been called - // expect(toast.error).toHaveBeenCalledWith('An error occurred'); - // }); - - // it('displays no event found message', async () => { - // const noEventMock = [ - // { - // request: { - // query: EVENT_DETAILS, - // variables: { id: 'event123' }, - // }, - // result: { - // data: { - // event: { - // _id: null, - // title: null, - // startDate: null, - // description: null, - // endDate: null, - // startTime: null, - // endTime: null, - // allDay: false, - // location: null, - // recurring: null, - // organization: { - // _id: null, - // members: [], - // }, - // baseRecurringEvent: { - // _id: 'recurringEvent123', - // }, - // attendees: [], - // }, - // }, - // }, - // }, - // ]; - - // render( - // - // - // - // - // - //
- //
, - // ); - - // await waitFor(() => screen.getByText('No event found')); - // expect(screen.getByText('No event found')).toBeInTheDocument(); - // }); -}); diff --git a/src/components/OrgContriCards/OrgContriCards.module.css b/src/components/OrgContriCards/OrgContriCards.module.css deleted file mode 100644 index d20b696621..0000000000 --- a/src/components/OrgContriCards/OrgContriCards.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.cards { - width: 45%; - background: #fcfcfc; - margin: 10px 20px; - padding: 20px 30px; - border-radius: 5px; - border: 1px solid #e8e8e8; - box-shadow: 0 3px 5px #c9c9c9; - margin-right: 40px; - color: #737373; -} -.cards > h2 { - font-size: 19px; -} -.cards > h3 { - font-size: 17px; -} -.cards > p { - font-size: 14px; - margin-top: -5px; - margin-bottom: 7px; -} diff --git a/src/components/OrgContriCards/OrgContriCards.tsx b/src/components/OrgContriCards/OrgContriCards.tsx index 6635be09b8..84237013c8 100644 --- a/src/components/OrgContriCards/OrgContriCards.tsx +++ b/src/components/OrgContriCards/OrgContriCards.tsx @@ -3,7 +3,7 @@ import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import { useTranslation } from 'react-i18next'; -import styles from './OrgContriCards.module.css'; +import styles from '../../style/app.module.css'; /** * Props for the OrgContriCards component diff --git a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx index d3698bf346..784d69325f 100644 --- a/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx +++ b/src/components/OrgSettings/ActionItemCategories/OrgActionItemCategories.test.tsx @@ -216,11 +216,15 @@ describe('Testing Organisation Action Item Categories', () => { const searchInput = await screen.findByTestId('searchByName'); expect(searchInput).toBeInTheDocument(); - userEvent.type(searchInput, 'Category 1'); - userEvent.type(searchInput, '{enter}'); + // Simulate typing and pressing ENTER + userEvent.type(searchInput, 'Category 1{enter}'); + + // Wait for the filtering to complete await waitFor(() => { - expect(screen.getByText('Category 1')).toBeInTheDocument(); - expect(screen.queryByText('Category 2')).toBeNull(); + // Assert only "Category 1" is visible + const categories = screen.getAllByTestId('categoryName'); + expect(categories).toHaveLength(1); + expect(categories[0]).toHaveTextContent('Category 1'); }); }); diff --git a/src/components/OrgSettings/General/GeneralSettings.tsx b/src/components/OrgSettings/General/GeneralSettings.tsx index 4dbca1b6eb..739456150a 100644 --- a/src/components/OrgSettings/General/GeneralSettings.tsx +++ b/src/components/OrgSettings/General/GeneralSettings.tsx @@ -1,6 +1,6 @@ import React, { type FC } from 'react'; import { Card, Col, Form, Row } from 'react-bootstrap'; -import styles from 'screens/OrgSettings/OrgSettings.module.css'; +import styles from '../../../../src/style/app.module.css'; import OrgProfileFieldSettings from './OrgProfileFieldSettings/OrgProfileFieldSettings'; import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; import DeleteOrg from './DeleteOrg/DeleteOrg'; diff --git a/src/components/OrganizationScreen/OrganizationScreen.test.tsx b/src/components/OrganizationScreen/OrganizationScreen.spec.tsx similarity index 88% rename from src/components/OrganizationScreen/OrganizationScreen.test.tsx rename to src/components/OrganizationScreen/OrganizationScreen.spec.tsx index cd039cc3ca..e6a75c46d8 100644 --- a/src/components/OrganizationScreen/OrganizationScreen.test.tsx +++ b/src/components/OrganizationScreen/OrganizationScreen.spec.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { I18nextProvider } from 'react-i18next'; -import 'jest-location-mock'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { store } from 'state/store'; @@ -11,10 +10,10 @@ import OrganizationScreen from './OrganizationScreen'; import { ORGANIZATION_EVENT_LIST } from 'GraphQl/Queries/Queries'; import { StaticMockLink } from 'utils/StaticMockLink'; import styles from './OrganizationScreen.module.css'; - +import { vi } from 'vitest'; const mockID: string | undefined = '123'; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), useParams: () => ({ orgId: mockID }), useMatch: () => ({ params: { eventId: 'event123', orgId: '123' } }), })); @@ -80,24 +79,19 @@ describe('Testing OrganizationScreen', () => { const closeButton = screen.getByTestId('closeMenu'); fireEvent.click(closeButton); - // Check for contract class after closing - expect(screen.getByTestId('mainpageright')).toHaveClass('_expand_ccl5z_8'); + expect(screen.getByTestId('mainpageright')).toHaveClass(styles.expand); const openButton = screen.getByTestId('openMenu'); fireEvent.click(openButton); // Check for expand class after opening - expect(screen.getByTestId('mainpageright')).toHaveClass( - '_contract_ccl5z_61', - ); + expect(screen.getByTestId('mainpageright')).toHaveClass(styles.contract); }); test('handles window resize', () => { renderComponent(); - window.innerWidth = 800; fireEvent(window, new Event('resize')); - expect(screen.getByTestId('mainpageright')).toHaveClass(styles.expand); }); }); diff --git a/src/screens/EventVolunteers/EventVolunteers.module.css b/src/screens/EventVolunteers/EventVolunteers.module.css deleted file mode 100644 index 84b19f0a9f..0000000000 --- a/src/screens/EventVolunteers/EventVolunteers.module.css +++ /dev/null @@ -1,266 +0,0 @@ -/* Toggle Btn */ -.toggleGroup { - width: 50%; - min-width: 20rem; - margin: 0.5rem 0rem; -} - -.toggleBtn { - padding: 0rem; - height: 2rem; - display: flex; - justify-content: center; - align-items: center; -} - -.toggleBtn:hover { - color: #31bb6b !important; -} - -input[type='radio']:checked + label { - background-color: #31bb6a50 !important; -} - -input[type='radio']:checked + label:hover { - color: black !important; -} - -.actionItemsContainer { - height: 90vh; -} - -.actionItemModal { - max-width: 80vw; - margin-top: 2vh; - margin-left: 13vw; -} - -.datediv { - display: flex; - flex-direction: row; -} - -.datebox { - width: 90%; - border-radius: 7px; - outline: none; - box-shadow: none; - padding-top: 2px; - padding-bottom: 2px; - padding-right: 5px; - padding-left: 5px; - margin-right: 5px; - margin-left: 5px; -} - -.dropdownToggle { - margin-bottom: 0; - display: flex; -} - -.dropdownModalToggle { - width: 50%; -} - -.errorIcon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.greenregbtn { - margin: 1rem 0 0; - margin-top: 15px; - border: 1px solid var(--bs-gray-300); - box-shadow: 0 2px 2px var(--bs-gray-300); - padding: 10px 10px; - border-radius: 5px; - background-color: var(--bs-primary); - width: 100%; - font-size: 16px; - color: var(--bs-white); - outline: none; - font-weight: 600; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s; - width: 100%; -} - -hr { - border: none; - height: 1px; - background-color: var(--bs-gray-500); - margin: 1rem; -} - -.iconContainer { - display: flex; - justify-content: flex-end; -} -.icon { - margin: 1px; -} - -.message { - margin-top: 25%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.preview { - display: flex; - flex-direction: row; - font-weight: 900; - font-size: 16px; - color: rgb(80, 80, 80); -} - -.removeFilterIcon { - cursor: pointer; -} - -.searchForm { - display: inline; -} - -.view { - margin-left: 2%; - font-weight: 600; - font-size: 16px; - color: var(--bs-gray-600); -} - -/* header (search, filter, dropdown) */ -.btnsContainer { - display: flex; - margin: 0.5rem 0 1.5rem 0; -} - -.btnsContainer .input { - flex: 1; - min-width: 18rem; - position: relative; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .input button { - width: 52px; -} - -.noOutline input { - outline: none; -} - -.noOutline input:disabled { - -webkit-text-fill-color: black !important; -} - -.noOutline textarea:disabled { - -webkit-text-fill-color: black !important; -} - -.inputField { - margin-top: 10px; - margin-bottom: 10px; - background-color: white; - box-shadow: 0 1px 1px #31bb6b; -} - -.inputField > button { - padding-top: 10px; - padding-bottom: 10px; -} - -.dropdown { - background-color: white; - border: 1px solid #31bb6b; - position: relative; - display: inline-block; - color: #31bb6b; -} - -/* Action Items Data Grid */ -.rowBackground { - background-color: var(--bs-white); - max-height: 120px; -} - -.tableHeader { - background-color: var(--bs-primary); - color: var(--bs-white); - font-size: 1rem; -} - -.chipIcon { - height: 0.9rem !important; -} - -.chip { - height: 1.5rem !important; -} - -.active { - background-color: #31bb6a50 !important; -} - -.pending { - background-color: #ffd76950 !important; - color: #bb952bd0 !important; - border-color: #bb952bd0 !important; -} - -/* Modals */ -.itemModal { - max-width: 80vw; - margin-top: 2vh; - margin-left: 13vw; -} - -.titlemodal { - color: #707070; - font-weight: 600; - font-size: 32px; - width: 65%; - margin-bottom: 0px; -} - -.modalCloseBtn { - width: 40px; - height: 40px; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.imageContainer { - display: flex; - align-items: center; - justify-content: center; - margin-right: 0.5rem; -} - -.TableImage { - object-fit: cover; - width: 25px !important; - height: 25px !important; - border-radius: 100% !important; -} - -.avatarContainer { - width: 28px; - height: 26px; -} - -/* Modal Table (Groups & Assignments) */ -.modalTable { - max-height: 220px; - overflow-y: auto; -} diff --git a/src/screens/EventVolunteers/Requests/Requests.test.tsx b/src/screens/EventVolunteers/Requests/Requests.spec.tsx similarity index 92% rename from src/screens/EventVolunteers/Requests/Requests.test.tsx rename to src/screens/EventVolunteers/Requests/Requests.spec.tsx index 3b55ea872c..e51e28ab3f 100644 --- a/src/screens/EventVolunteers/Requests/Requests.test.tsx +++ b/src/screens/EventVolunteers/Requests/Requests.spec.tsx @@ -1,3 +1,10 @@ +/** + * Testing component for managing and displaying Volunteer Membership requests for an event. + * + * This component allows users to view, filter, sort, and create action items. It also allows users to accept or reject volunteer membership requests. + * + * + */ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { LocalizationProvider } from '@mui/x-date-pickers'; @@ -20,11 +27,12 @@ import { UPDATE_ERROR_MOCKS, } from './Requests.mocks'; import { toast } from 'react-toastify'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -74,14 +82,14 @@ const renderRequests = (link: ApolloLink): RenderResult => { describe('Testing Requests Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), + vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), useParams: () => ({ orgId: 'orgId', eventId: 'eventId' }), })); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { @@ -102,10 +110,7 @@ describe('Testing Requests Screen', () => { , ); - - await waitFor(() => { - expect(screen.getByTestId('paramsError')).toBeInTheDocument(); - }); + expect(window.location.pathname).toBe('/'); }); it('should render Requests screen', async () => { diff --git a/src/screens/EventVolunteers/Requests/Requests.tsx b/src/screens/EventVolunteers/Requests/Requests.tsx index 41abcad763..b19be3d2a0 100644 --- a/src/screens/EventVolunteers/Requests/Requests.tsx +++ b/src/screens/EventVolunteers/Requests/Requests.tsx @@ -13,7 +13,7 @@ import { type GridColDef, } from '@mui/x-data-grid'; import Avatar from 'components/Avatar/Avatar'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import { USER_VOLUNTEER_MEMBERSHIP } from 'GraphQl/Queries/EventVolunteerQueries'; import type { InterfaceVolunteerMembership } from 'utils/interfaces'; import dayjs from 'dayjs'; @@ -154,7 +154,7 @@ function requests(): JSX.Element { align: 'center', headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return params.row.id; }, @@ -167,7 +167,7 @@ function requests(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { const { firstName, lastName, image } = params.row.volunteer.user; return ( @@ -180,14 +180,14 @@ function requests(): JSX.Element { src={image} alt="volunteer" data-testid={`volunteer_image`} - className={styles.TableImage} + className={styles.TableImages} /> ) : (
@@ -205,7 +205,7 @@ function requests(): JSX.Element { minWidth: 150, align: 'center', headerAlign: 'center', - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, sortable: false, renderCell: (params: GridCellParams) => { return dayjs(params.row.createdAt).format('DD/MM/YYYY'); @@ -219,7 +219,7 @@ function requests(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return ( <> @@ -251,7 +251,7 @@ function requests(): JSX.Element { return (
{/* Header with search, filter and Create Button */} -
+
{ setSearchValue(e.target.value); @@ -282,7 +282,7 @@ function requests(): JSX.Element { @@ -316,7 +316,7 @@ function requests(): JSX.Element { hideFooter={true} getRowId={(row) => row._id} sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} + getRowClassName={() => `${styles.rowBackgrounds}`} autoHeight rowHeight={65} rows={requests.map((request, index) => ({ diff --git a/src/screens/EventVolunteers/VolunteerContainer.tsx b/src/screens/EventVolunteers/VolunteerContainer.tsx index 1a425a706e..e026c6f7c8 100644 --- a/src/screens/EventVolunteers/VolunteerContainer.tsx +++ b/src/screens/EventVolunteers/VolunteerContainer.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Navigate, useParams } from 'react-router-dom'; -import styles from './EventVolunteers.module.css'; +import styles from '../../style/app.module.css'; import { HiUserGroup, HiUser } from 'react-icons/hi2'; import Volunteers from './Volunteers/Volunteers'; import VolunteerGroups from './VolunteerGroups/VolunteerGroups'; diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.test.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.spec.tsx similarity index 92% rename from src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.test.tsx rename to src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.spec.tsx index 05c2dab5ff..8e726028db 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.test.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.spec.tsx @@ -16,11 +16,18 @@ import { toast } from 'react-toastify'; import type { InterfaceDeleteVolunteerGroupModal } from './VolunteerGroupDeleteModal'; import VolunteerGroupDeleteModal from './VolunteerGroupDeleteModal'; import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Mock implementation of the `react-toastify` module. + * Mocks the `toast` object with `success` and `error` methods to allow testing + * without triggering actual toast notifications. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -39,8 +46,8 @@ const t = { const itemProps: InterfaceDeleteVolunteerGroupModal[] = [ { isOpen: true, - hide: jest.fn(), - refetchGroups: jest.fn(), + hide: vi.fn(), + refetchGroups: vi.fn(), group: { _id: 'groupId', name: 'Group 1', diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.tsx index 33132bfd33..89c788e220 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupDeleteModal.tsx @@ -1,5 +1,5 @@ import { Button, Modal } from 'react-bootstrap'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from '@apollo/client'; diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.test.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.spec.tsx similarity index 96% rename from src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.test.tsx rename to src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.spec.tsx index 2fc0b2e348..79b1d94545 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.test.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.spec.tsx @@ -22,11 +22,18 @@ import { toast } from 'react-toastify'; import type { InterfaceVolunteerGroupModal } from './VolunteerGroupModal'; import GroupModal from './VolunteerGroupModal'; import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Mock implementation of the `react-toastify` module. + * Mocks the `toast` object with `success` and `error` methods to allow testing + * without triggering actual toast notifications. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -45,19 +52,19 @@ const t = { const itemProps: InterfaceVolunteerGroupModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', orgId: 'orgId', - refetchGroups: jest.fn(), + refetchGroups: vi.fn(), mode: 'create', group: null, }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', orgId: 'orgId', - refetchGroups: jest.fn(), + refetchGroups: vi.fn(), mode: 'edit', group: { _id: 'groupId', @@ -96,10 +103,10 @@ const itemProps: InterfaceVolunteerGroupModal[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', orgId: 'orgId', - refetchGroups: jest.fn(), + refetchGroups: vi.fn(), mode: 'edit', group: { _id: 'groupId', diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.tsx index 5bfb1eff2b..e36ecaa0bd 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupModal.tsx @@ -5,7 +5,7 @@ import type { InterfaceUserInfo, InterfaceVolunteerGroupInfo, } from 'utils/interfaces'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from '@apollo/client'; diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.test.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.spec.tsx similarity index 98% rename from src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.test.tsx rename to src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.spec.tsx index 94c34923a2..b029909809 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.test.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.spec.tsx @@ -11,6 +11,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import i18n from 'utils/i18nForTest'; import type { InterfaceVolunteerGroupViewModal } from './VolunteerGroupViewModal'; import VolunteerGroupViewModal from './VolunteerGroupViewModal'; +import { vi } from 'vitest'; const t = { ...JSON.parse( @@ -25,7 +26,7 @@ const t = { const itemProps: InterfaceVolunteerGroupViewModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), group: { _id: 'groupId', name: 'Group 1', @@ -63,7 +64,7 @@ const itemProps: InterfaceVolunteerGroupViewModal[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), group: { _id: 'groupId', name: 'Group 1', diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.tsx index 70994bd4e5..5fb090649f 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal.tsx @@ -1,6 +1,6 @@ import { Button, Form, Modal } from 'react-bootstrap'; import type { InterfaceVolunteerGroupInfo } from 'utils/interfaces'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -123,14 +123,14 @@ const VolunteerGroupViewModal: React.FC = ({ src={leader.image} alt="Volunteer" data-testid="leader_image" - className={styles.TableImage} + className={styles.TableImages} /> ) : (
= ({ src={creator.image} alt="Volunteer" data-testid="creator_image" - className={styles.TableImage} + className={styles.TableImages} /> ) : (
{ ); }; +/** Mock useParams to provide consistent test data */ + describe('Testing VolunteerGroups Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId', eventId: 'eventId' }), - })); + vi.mock('react-router-dom', async () => { + const actualDom = await vi.importActual('react-router-dom'); + return { + ...actualDom, + useParams: vi.fn(), + }; + }); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); + const mockRouteParams = (orgId = 'orgId', eventId = 'eventId'): void => { + vi.mocked(useParams).mockReturnValue({ orgId, eventId }); + }; + it('should redirect to fallback URL if URL params are undefined', async () => { + /** Mocking the useParams hook to return undefined parameters */ + mockRouteParams('', ''); render( @@ -98,12 +110,14 @@ describe('Testing VolunteerGroups Screen', () => { }); it('should render Groups screen', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); }); it('Check Sorting Functionality', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); @@ -133,6 +147,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Search by Groups', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); @@ -153,6 +168,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Search by Leader', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); @@ -174,6 +190,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('should render screen with No Groups', async () => { + mockRouteParams(); renderVolunteerGroups(link3); await waitFor(() => { @@ -183,6 +200,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Error while fetching groups data', async () => { + mockRouteParams(); renderVolunteerGroups(link2); await waitFor(() => { @@ -191,6 +209,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Open and close ViewModal', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const viewGroupBtn = await screen.findAllByTestId('viewGroupBtn'); @@ -201,6 +220,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Open and Close Delete Modal', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const deleteGroupBtn = await screen.findAllByTestId('deleteGroupBtn'); @@ -211,6 +231,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Open and close GroupModal (Edit)', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const editGroupBtn = await screen.findAllByTestId('editGroupBtn'); @@ -221,6 +242,7 @@ describe('Testing VolunteerGroups Screen', () => { }); it('Open and close GroupModal (Create)', async () => { + mockRouteParams(); renderVolunteerGroups(link1); const createGroupBtn = await screen.findByTestId('createGroupBtn'); diff --git a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx index fa98abc9f2..3c70b1db49 100644 --- a/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx +++ b/src/screens/EventVolunteers/VolunteerGroups/VolunteerGroups.tsx @@ -16,7 +16,7 @@ import { } from '@mui/x-data-grid'; import { debounce, Stack } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import { EVENT_VOLUNTEER_GROUP_LIST } from 'GraphQl/Queries/EventVolunteerQueries'; import VolunteerGroupModal from './VolunteerGroupModal'; import VolunteerGroupDeleteModal from './VolunteerGroupDeleteModal'; @@ -161,7 +161,7 @@ function volunteerGroups(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return (
{ const { _id, firstName, lastName, image } = params.row.leader; return ( @@ -194,14 +194,14 @@ function volunteerGroups(): JSX.Element { src={image} alt="Assignee" data-testid={`image${_id + 1}`} - className={styles.TableImage} + className={styles.TableImages} /> ) : (
@@ -219,7 +219,7 @@ function volunteerGroups(): JSX.Element { align: 'center', headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return (
@@ -235,7 +235,7 @@ function volunteerGroups(): JSX.Element { align: 'center', headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return (
@@ -252,7 +252,7 @@ function volunteerGroups(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return ( <> @@ -293,7 +293,7 @@ function volunteerGroups(): JSX.Element { return (
{/* Header with search, filter and Create Button */} -
+
{ setSearchValue(e.target.value); @@ -325,7 +325,7 @@ function volunteerGroups(): JSX.Element { @@ -350,7 +350,7 @@ function volunteerGroups(): JSX.Element { @@ -400,7 +400,7 @@ function volunteerGroups(): JSX.Element { ), }} sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} + getRowClassName={() => `${styles.rowBackgrounds}`} autoHeight rowHeight={65} rows={groups.map((group, index) => ({ diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.test.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.spec.tsx similarity index 91% rename from src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.test.tsx rename to src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.spec.tsx index cac8fe94f0..77fe028655 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.test.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.spec.tsx @@ -22,11 +22,18 @@ import { toast } from 'react-toastify'; import type { InterfaceVolunteerCreateModal } from './VolunteerCreateModal'; import VolunteerCreateModal from './VolunteerCreateModal'; import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Mock implementation of the `react-toastify` module. + * Mocks the `toast` object with `success` and `error` methods to allow testing + * without triggering actual toast notifications. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -45,10 +52,10 @@ const t = { const itemProps: InterfaceVolunteerCreateModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), eventId: 'eventId', orgId: 'orgId', - refetchVolunteers: jest.fn(), + refetchVolunteers: vi.fn(), }, ]; diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.tsx index 6b4a1e3f0c..dee45376db 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerCreateModal.tsx @@ -1,7 +1,7 @@ import type { ChangeEvent } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; import type { InterfaceUserInfo } from 'utils/interfaces'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from '@apollo/client'; diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.test.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.spec.tsx similarity index 91% rename from src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.test.tsx rename to src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.spec.tsx index dd9d6d5985..575670a887 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.test.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.spec.tsx @@ -15,11 +15,18 @@ import { toast } from 'react-toastify'; import type { InterfaceDeleteVolunteerModal } from './VolunteerDeleteModal'; import VolunteerDeleteModal from './VolunteerDeleteModal'; import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Mock implementation of the `react-toastify` module. + * Mocks the `toast` object with `success` and `error` methods to allow testing + * without triggering actual toast notifications. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -38,8 +45,8 @@ const t = { const itemProps: InterfaceDeleteVolunteerModal[] = [ { isOpen: true, - hide: jest.fn(), - refetchVolunteers: jest.fn(), + hide: vi.fn(), + refetchVolunteers: vi.fn(), volunteer: { _id: 'volunteerId1', hasAccepted: true, diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.tsx index 8f253fdf50..5c841a2f11 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerDeleteModal.tsx @@ -1,5 +1,5 @@ import { Button, Modal } from 'react-bootstrap'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from '@apollo/client'; diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.test.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.spec.tsx similarity index 97% rename from src/screens/EventVolunteers/Volunteers/VolunteerViewModal.test.tsx rename to src/screens/EventVolunteers/Volunteers/VolunteerViewModal.spec.tsx index 155dba8464..e99fb47d20 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.test.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.spec.tsx @@ -10,6 +10,7 @@ import { store } from 'state/store'; import i18n from 'utils/i18nForTest'; import type { InterfaceVolunteerViewModal } from './VolunteerViewModal'; import VolunteerViewModal from './VolunteerViewModal'; +import { vi } from 'vitest'; const t = { ...JSON.parse( @@ -24,7 +25,7 @@ const t = { const itemProps: InterfaceVolunteerViewModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), volunteer: { _id: 'volunteerId1', hasAccepted: true, @@ -51,7 +52,7 @@ const itemProps: InterfaceVolunteerViewModal[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), volunteer: { _id: 'volunteerId2', hasAccepted: false, diff --git a/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.tsx b/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.tsx index 0904d34b9c..830bacf8cc 100644 --- a/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.tsx +++ b/src/screens/EventVolunteers/Volunteers/VolunteerViewModal.tsx @@ -1,6 +1,6 @@ import { Button, Form, Modal } from 'react-bootstrap'; import type { InterfaceEventVolunteerInfo } from 'utils/interfaces'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -83,14 +83,14 @@ const VolunteerViewModal: React.FC = ({ src={user.image} alt="Volunteer" data-testid="volunteer_image" - className={styles.TableImage} + className={styles.TableImages} /> ) : (
{ ); }; +/** Mock useParams to provide consistent test data */ + describe('Testing Volunteers Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId', eventId: 'eventId' }), - })); + vi.mock('react-router-dom', async () => { + const actualDom = await vi.importActual('react-router-dom'); + return { + ...actualDom, + useParams: vi.fn(), + }; + }); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: '', eventId: '' }); render( @@ -95,12 +102,21 @@ describe('Testing Volunteers Screen', () => { }); it('should render Volunteers screen', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); + renderVolunteers(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); }); it('Check Sorting Functionality', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); @@ -134,6 +150,10 @@ describe('Testing Volunteers Screen', () => { }); it('Filter Volunteers by status (All)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const filterBtn = await screen.findByTestId('filter'); @@ -151,6 +171,10 @@ describe('Testing Volunteers Screen', () => { }); it('Filter Volunteers by status (Pending)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const filterBtn = await screen.findByTestId('filter'); @@ -168,6 +192,10 @@ describe('Testing Volunteers Screen', () => { }); it('Filter Volunteers by status (Accepted)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const filterBtn = await screen.findByTestId('filter'); @@ -185,6 +213,10 @@ describe('Testing Volunteers Screen', () => { }); it('Search', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const searchInput = await screen.findByTestId('searchBy'); expect(searchInput).toBeInTheDocument(); @@ -197,6 +229,10 @@ describe('Testing Volunteers Screen', () => { }); it('should render screen with No Volunteers', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link3); await waitFor(() => { @@ -206,6 +242,10 @@ describe('Testing Volunteers Screen', () => { }); it('Error while fetching volunteers data', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link2); await waitFor(() => { @@ -214,6 +254,10 @@ describe('Testing Volunteers Screen', () => { }); it('Open and close Volunteer Modal (View)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const viewItemBtn = await screen.findAllByTestId('viewItemBtn'); @@ -224,6 +268,10 @@ describe('Testing Volunteers Screen', () => { }); it('Open and Close Volunteer Modal (Delete)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const deleteItemBtn = await screen.findAllByTestId('deleteItemBtn'); @@ -234,6 +282,10 @@ describe('Testing Volunteers Screen', () => { }); it('Open and close Volunteer Modal (Create)', async () => { + vi.mocked(useParams).mockReturnValue({ + orgId: 'orgId', + eventId: 'eventId', + }); renderVolunteers(link1); const addVolunteerBtn = await screen.findByTestId('addVolunteerBtn'); diff --git a/src/screens/EventVolunteers/Volunteers/Volunteers.tsx b/src/screens/EventVolunteers/Volunteers/Volunteers.tsx index 770bd35ef4..875431ad6f 100644 --- a/src/screens/EventVolunteers/Volunteers/Volunteers.tsx +++ b/src/screens/EventVolunteers/Volunteers/Volunteers.tsx @@ -20,7 +20,7 @@ import { } from '@mui/x-data-grid'; import { Chip, debounce, Stack } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; -import styles from '../EventVolunteers.module.css'; +import styles from '../../../style/app.module.css'; import { EVENT_VOLUNTEER_LIST } from 'GraphQl/Queries/EventVolunteerQueries'; import type { InterfaceEventVolunteerInfo } from 'utils/interfaces'; import VolunteerCreateModal from './VolunteerCreateModal'; @@ -179,7 +179,7 @@ function volunteers(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { const { _id, firstName, lastName, image } = params.row.user; return ( @@ -192,7 +192,7 @@ function volunteers(): JSX.Element { src={image} alt="volunteer" data-testid="volunteer_image" - className={styles.TableImage} + className={styles.TableImages} /> ) : (
@@ -200,7 +200,7 @@ function volunteers(): JSX.Element { key={_id + '1'} dataTestId="volunteer_avatar" containerStyle={styles.imageContainer} - avatarStyle={styles.TableImage} + avatarStyle={styles.TableImages} name={firstName + ' ' + lastName} alt={firstName + ' ' + lastName} /> @@ -219,7 +219,7 @@ function volunteers(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return ( { return (
{ return ( @@ -278,7 +278,7 @@ function volunteers(): JSX.Element { minWidth: 100, headerAlign: 'center', sortable: false, - headerClassName: `${styles.tableHeader}`, + headerClassName: `${styles.tableHeaders}`, renderCell: (params: GridCellParams) => { return ( <> @@ -310,7 +310,7 @@ function volunteers(): JSX.Element { return (
{/* Header with search, filter and Create Button */} -
+
{ setSearchValue(e.target.value); @@ -341,7 +341,7 @@ function volunteers(): JSX.Element { @@ -365,7 +365,7 @@ function volunteers(): JSX.Element { @@ -421,7 +421,7 @@ function volunteers(): JSX.Element { ), }} sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} + getRowClassName={() => `${styles.rowBackgrounds}`} autoHeight rowHeight={65} rows={volunteers.map((volunteer, index) => ({ diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.module.css b/src/screens/FundCampaignPledge/FundCampaignPledge.module.css deleted file mode 100644 index cdf4476267..0000000000 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.module.css +++ /dev/null @@ -1,273 +0,0 @@ -.pledgeContainer { - margin: 0.6rem 0; -} - -.container { - min-height: 100vh; -} - -.pledgeModal { - max-width: 80vw; - margin-top: 2vh; - margin-left: 13vw; -} - -.titlemodal { - color: #707070; - font-weight: 600; - font-size: 32px; - width: 65%; - margin-bottom: 0px; -} - -.modalCloseBtn { - width: 40px; - height: 40px; - padding: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.greenregbtn { - margin: 1rem 0 0; - margin-top: 15px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 10px; - border-radius: 5px; - background-color: #31bb6b; - width: 100%; - font-size: 16px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s; - width: 100%; -} -.message { - margin-top: 25%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.errorIcon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.btnsContainer { - display: flex; - gap: 0.8rem; - margin: 2.2rem 0 0.8rem 0; -} - -.btnsContainer .input { - flex: 1; - min-width: 18rem; - position: relative; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .input button { - width: 52px; -} - -.inputField { - background-color: white; - box-shadow: 0 1px 1px #31bb6b; -} - -.dropdown { - background-color: white; - border: 1px solid #31bb6b; - position: relative; - display: inline-block; - color: #31bb6b; -} - -.tableHeader { - background-color: var(--bs-primary); - color: var(--bs-white); - font-size: 1rem; -} - -.rowBackground { - background-color: var(--bs-white); - max-height: 120px; -} - -.TableImage { - object-fit: cover; - width: 25px !important; - height: 25px !important; - border-radius: 100% !important; -} - -.avatarContainer { - width: 28px; - height: 26px; -} - -.imageContainer { - display: flex; - align-items: center; - justify-content: center; -} - -.pledgerContainer { - display: flex; - align-items: center; - justify-content: center; - margin: 0.1rem 0.25rem; - gap: 0.25rem; - padding: 0.25rem 0.45rem; - border-radius: 0.35rem; - background-color: #31bb6b33; - height: 2.2rem; - margin-top: 0.75rem; -} - -.noOutline input { - outline: none; -} - -.overviewContainer { - display: flex; - gap: 7rem; - width: 100%; - justify-content: space-between; - margin: 1.5rem 0 0 0; - padding: 1.25rem 2rem; - background-color: rgba(255, 255, 255, 0.591); - - box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; - border-radius: 0.5rem; -} - -.titleContainer { - display: flex; - flex-direction: column; - gap: 0.6rem; -} - -.titleContainer h3 { - font-size: 1.75rem; - font-weight: 750; - color: #5e5e5e; - margin-top: 0.2rem; -} - -.titleContainer span { - font-size: 0.9rem; - margin-left: 0.5rem; - font-weight: lighter; - color: #707070; -} - -.raisedAmount { - display: flex; - justify-content: center; - align-items: center; - font-size: 1.25rem; - font-weight: 750; - color: #5e5e5e; -} - -.progressContainer { - display: flex; - flex-direction: column; - gap: 0.5rem; - flex-grow: 1; -} - -.progress { - margin-top: 0.2rem; - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -.endpoints { - display: flex; - position: relative; - font-size: 0.85rem; -} - -.start { - position: absolute; - top: 0px; -} - -.end { - position: absolute; - top: 0px; - right: 0px; -} - -.moreContainer { - display: flex; - align-items: center; -} - -.moreContainer:hover { - text-decoration: underline; - cursor: pointer; -} - -.popup { - z-index: 50; - border-radius: 0.5rem; - font-family: sans-serif; - font-weight: 500; - font-size: 0.875rem; - margin-top: 0.5rem; - padding: 0.75rem; - border: 1px solid #e2e8f0; - background-color: white; - color: #1e293b; - box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 0.15); - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.popupExtra { - max-height: 15rem; - overflow-y: auto; -} - -.toggleGroup { - width: 50%; - min-width: 27.75rem; - margin: 0.5rem 0rem; -} - -.toggleBtn { - padding: 0rem; - height: 30px; - display: flex; - justify-content: center; - align-items: center; -} - -.toggleBtn:hover { - color: #31bb6b !important; -} - -input[type='radio']:checked + label { - background-color: #31bb6a50 !important; -} - -input[type='radio']:checked + label:hover { - color: black !important; -} diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx index d14ee9de06..8942265eea 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx @@ -9,7 +9,7 @@ import { Button, Dropdown, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Navigate, useParams } from 'react-router-dom'; import { currencySymbols } from 'utils/currency'; -import styles from './FundCampaignPledge.module.css'; +import styles from '../../style/app.module.css'; import PledgeDeleteModal from './PledgeDeleteModal'; import PledgeModal from './PledgeModal'; import { Breadcrumbs, Link, Stack, Typography } from '@mui/material'; @@ -233,14 +233,14 @@ const fundCampaignPledge = (): JSX.Element => { src={user.image} alt="pledge" data-testid={`image${index + 1}`} - className={styles.TableImage} + className={styles.TableImagePledge} /> ) : (
@@ -425,14 +425,14 @@ const fundCampaignPledge = (): JSX.Element => { > setProgressIndicator('pledged')} />
-
-
+
+
setSearchTerm(e.target.value)} data-testid="searchPledger" @@ -557,7 +557,7 @@ const fundCampaignPledge = (): JSX.Element => { ), }} sx={dataGridStyle} - getRowClassName={() => `${styles.rowBackground}`} + getRowClassName={() => `${styles.rowBackgroundPledge}`} autoHeight rowHeight={65} rows={pledges.map((pledge) => ({ @@ -607,14 +607,14 @@ const fundCampaignPledge = (): JSX.Element => { src={user.image} alt="pledger" data-testid={`extraImage${index + 1}`} - className={styles.TableImage} + className={styles.TableImagePledge} /> ) : (
= ({ = ({ format="DD/MM/YYYY" label={tCommon('startDate')} value={dayjs(pledgeStartDate)} - className={styles.noOutline} + className={styles.noOutlinePledge} onChange={(date: Dayjs | null): void => { if (date) { setFormState({ @@ -280,7 +280,7 @@ const PledgeModal: React.FC = ({ { if (date) { @@ -327,7 +327,7 @@ const PledgeModal: React.FC = ({ { if (parseInt(e.target.value) > 0) { @@ -343,7 +343,7 @@ const PledgeModal: React.FC = ({ {/* Button to submit the pledge form */}
) : ( - /* istanbul ignore next */ <> )}
) : !isLoading && orgsData?.organizationsConnection.length == 0 && - /* istanbul ignore next */ searchByName.length > 0 ? ( - /* istanbul ignore next */

{tCommon('noResultsFoundFor')} "{searchByName}" diff --git a/src/screens/OrgSettings/OrgSettings.module.css b/src/screens/OrgSettings/OrgSettings.module.css deleted file mode 100644 index 9952a9a459..0000000000 --- a/src/screens/OrgSettings/OrgSettings.module.css +++ /dev/null @@ -1,55 +0,0 @@ -.headerBtn { - box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 2px; -} -.settingsContainer { - min-height: 100vh; -} - -.settingsBody { - min-height: 100vh; - margin: 2.5rem 1rem; -} - -.cardHeader { - padding: 1.25rem 1rem 1rem 1rem; - border-bottom: 1px solid var(--bs-gray-200); - display: flex; - justify-content: space-between; - align-items: center; -} - -.cardHeader .cardTitle { - font-size: 1.2rem; - font-weight: 600; -} - -.cardBody { - min-height: 180px; -} - -.cardBody .textBox { - margin: 0 0 3rem 0; - color: var(--bs-secondary); -} - -hr { - border: none; - height: 1px; - background-color: var(--bs-gray-500); -} - -.settingsTabs { - display: none; -} - -@media (min-width: 577px) { - .settingsDropdown { - display: none; - } -} - -@media (min-width: 577px) { - .settingsTabs { - display: block; - } -} diff --git a/src/screens/OrgSettings/OrgSettings.test.tsx b/src/screens/OrgSettings/OrgSettings.spec.tsx similarity index 54% rename from src/screens/OrgSettings/OrgSettings.test.tsx rename to src/screens/OrgSettings/OrgSettings.spec.tsx index a9aec5f33d..5b5179d644 100644 --- a/src/screens/OrgSettings/OrgSettings.test.tsx +++ b/src/screens/OrgSettings/OrgSettings.spec.tsx @@ -1,28 +1,38 @@ +import type { ReactElement } from 'react'; import React from 'react'; -import { MockedProvider } from '@apollo/react-testing'; -import type { RenderResult } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; -import 'jest-location-mock'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; - +import { Provider } from 'react-redux'; +import { I18nextProvider } from 'react-i18next'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { MockedProvider } from '@apollo/react-testing'; import { store } from 'state/store'; -import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; import OrgSettings from './OrgSettings'; -import userEvent from '@testing-library/user-event'; -import type { ApolloLink } from '@apollo/client'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { MOCKS } from './OrgSettings.mocks'; const link1 = new StaticMockLink(MOCKS); - -const renderOrganisationSettings = (link: ApolloLink): RenderResult => { +const mockRouterParams = (orgId: string | undefined): void => { + vi.doMock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId }), + }; + }); +}; +const renderOrganisationSettings = ( + link = link1, + orgId = 'orgId', +): ReturnType => { + mockRouterParams(orgId); return render( - + @@ -30,7 +40,9 @@ const renderOrganisationSettings = (link: ApolloLink): RenderResult => { } />

} + element={ +
Redirected to Home
+ } /> @@ -42,28 +54,37 @@ const renderOrganisationSettings = (link: ApolloLink): RenderResult => { }; describe('Organisation Settings Page', () => { - beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + afterEach(() => { + vi.unmock('react-router-dom'); }); - afterAll(() => { - jest.clearAllMocks(); - }); + const SetupRedirectTest = async (): Promise => { + const useParamsMock = vi.fn(() => ({ orgId: undefined })); + vi.doMock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: useParamsMock, + }; + }); + const orgSettingsModule = await import('./OrgSettings'); + return ; + }; it('should redirect to fallback URL if URL params are undefined', async () => { + const OrgSettings = await SetupRedirectTest(); render( - + - } /> +
} + element={ +
Redirected to Home
+ } /> @@ -71,13 +92,16 @@ describe('Organisation Settings Page', () => { , ); + await waitFor(() => { - expect(screen.getByTestId('paramsError')).toBeInTheDocument(); + const paramsErrorElement = screen.getByTestId('paramsError'); + expect(paramsErrorElement).toBeInTheDocument(); + expect(paramsErrorElement.textContent).toBe('Redirected to Home'); }); }); - test('should render the organisation settings page', async () => { - renderOrganisationSettings(link1); + it('should render the organisation settings page', async () => { + renderOrganisationSettings(); await waitFor(() => { expect(screen.getByTestId('generalSettings')).toBeInTheDocument(); @@ -88,10 +112,11 @@ describe('Organisation Settings Page', () => { screen.getByTestId('agendaItemCategoriesSettings'), ).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('generalSettings')); + userEvent.click(screen.getByTestId('generalSettings')); await waitFor(() => { expect(screen.getByTestId('generalTab')).toBeInTheDocument(); + expect(screen.getByTestId('generalTab')).toBeVisible(); }); userEvent.click(screen.getByTestId('actionItemCategoriesSettings')); @@ -104,4 +129,19 @@ describe('Organisation Settings Page', () => { expect(screen.getByTestId('agendaItemCategoriesTab')).toBeInTheDocument(); }); }); + + it('should render dropdown for settings tabs', async () => { + renderOrganisationSettings(); + + await waitFor(() => { + expect(screen.getByTestId('settingsDropdownToggle')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByTestId('settingsDropdownToggle')); + + const dropdownItems = screen.getAllByRole('button', { + name: /general|actionItemCategories|agendaItemCategories/i, + }); + expect(dropdownItems).toHaveLength(3); + }); }); diff --git a/src/screens/OrgSettings/OrgSettings.tsx b/src/screens/OrgSettings/OrgSettings.tsx index e4ae5424a6..c7b01138ae 100644 --- a/src/screens/OrgSettings/OrgSettings.tsx +++ b/src/screens/OrgSettings/OrgSettings.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Button, Dropdown, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import styles from './OrgSettings.module.css'; +import styles from 'style/app.module.css'; import OrgActionItemCategories from 'components/OrgSettings/ActionItemCategories/OrgActionItemCategories'; import OrganizationAgendaCategory from 'components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory'; import { Navigate, useParams } from 'react-router-dom'; @@ -26,7 +26,7 @@ const settingtabs: SettingType[] = [ * * @returns The rendered component displaying the organization settings. */ -function orgSettings(): JSX.Element { +function OrgSettings(): JSX.Element { // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'orgSettings', @@ -126,4 +126,4 @@ function orgSettings(): JSX.Element { ); } -export default orgSettings; +export default OrgSettings; diff --git a/src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx b/src/screens/OrganizationActionItems/ItemDeleteModal.spec.tsx similarity index 91% rename from src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx rename to src/screens/OrganizationActionItems/ItemDeleteModal.spec.tsx index fffeebfd7f..5bdb1ffde1 100644 --- a/src/screens/OrganizationActionItems/ItemDeleteModal.test.tsx +++ b/src/screens/OrganizationActionItems/ItemDeleteModal.spec.tsx @@ -3,7 +3,13 @@ import type { ApolloLink } from '@apollo/client'; import { MockedProvider } from '@apollo/react-testing'; import { LocalizationProvider } from '@mui/x-date-pickers'; import type { RenderResult } from '@testing-library/react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + waitFor, + act, +} from '@testing-library/react'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -16,11 +22,12 @@ import { toast } from 'react-toastify'; import ItemDeleteModal, { type InterfaceItemDeleteModalProps, } from './ItemDeleteModal'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -34,8 +41,8 @@ const t = JSON.parse( const itemProps: InterfaceItemDeleteModalProps = { isOpen: true, - hide: jest.fn(), - actionItemsRefetch: jest.fn(), + hide: vi.fn(), + actionItemsRefetch: vi.fn(), actionItem: { _id: 'actionItemId1', assignee: null, @@ -102,7 +109,9 @@ describe('Testing ItemDeleteModal', () => { renderItemDeleteModal(link1, itemProps); expect(screen.getByTestId('deleteyesbtn')).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('deleteyesbtn')); + await act(() => { + fireEvent.click(screen.getByTestId('deleteyesbtn')); + }); await waitFor(() => { expect(itemProps.actionItemsRefetch).toHaveBeenCalled(); diff --git a/src/screens/OrganizationActionItems/ItemModal.test.tsx b/src/screens/OrganizationActionItems/ItemModal.spec.tsx similarity index 98% rename from src/screens/OrganizationActionItems/ItemModal.test.tsx rename to src/screens/OrganizationActionItems/ItemModal.spec.tsx index a58496e6df..1fe3d6fca5 100644 --- a/src/screens/OrganizationActionItems/ItemModal.test.tsx +++ b/src/screens/OrganizationActionItems/ItemModal.spec.tsx @@ -21,12 +21,13 @@ import { StaticMockLink } from 'utils/StaticMockLink'; import { toast } from 'react-toastify'; import type { InterfaceItemModalProps } from './ItemModal'; import ItemModal from './ItemModal'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), - warning: jest.fn(), + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), }, })); @@ -45,28 +46,28 @@ const t = { const itemProps: InterfaceItemModalProps[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: undefined, - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: false, actionItem: null, }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: 'eventId', - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: false, actionItem: null, }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: undefined, - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: true, actionItem: { _id: 'actionItemId1', @@ -106,10 +107,10 @@ const itemProps: InterfaceItemModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: undefined, - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: true, actionItem: { _id: 'actionItemId2', @@ -149,10 +150,10 @@ const itemProps: InterfaceItemModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: 'eventId', - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: true, actionItem: { _id: 'actionItemId2', @@ -202,10 +203,10 @@ const itemProps: InterfaceItemModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), orgId: 'orgId', eventId: 'eventId', - actionItemsRefetch: jest.fn(), + actionItemsRefetch: vi.fn(), editMode: true, actionItem: { _id: 'actionItemId2', diff --git a/src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.spec.tsx similarity index 96% rename from src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx rename to src/screens/OrganizationActionItems/ItemUpdateStatusModal.spec.tsx index aa28b14d40..4d9e16d11e 100644 --- a/src/screens/OrganizationActionItems/ItemUpdateStatusModal.test.tsx +++ b/src/screens/OrganizationActionItems/ItemUpdateStatusModal.spec.tsx @@ -16,11 +16,12 @@ import { toast } from 'react-toastify'; import ItemUpdateStatusModal, { type InterfaceItemUpdateStatusModalProps, } from './ItemUpdateStatusModal'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -35,8 +36,8 @@ const t = JSON.parse( const itemProps: InterfaceItemUpdateStatusModalProps[] = [ { isOpen: true, - hide: jest.fn(), - actionItemsRefetch: jest.fn(), + hide: vi.fn(), + actionItemsRefetch: vi.fn(), actionItem: { _id: 'actionItemId1', assignee: null, @@ -75,8 +76,8 @@ const itemProps: InterfaceItemUpdateStatusModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), - actionItemsRefetch: jest.fn(), + hide: vi.fn(), + actionItemsRefetch: vi.fn(), actionItem: { _id: 'actionItemId1', assignee: null, @@ -148,8 +149,8 @@ const itemProps: InterfaceItemUpdateStatusModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), - actionItemsRefetch: jest.fn(), + hide: vi.fn(), + actionItemsRefetch: vi.fn(), actionItem: { _id: 'actionItemId1', assignee: { diff --git a/src/screens/OrganizationActionItems/ItemViewModal.test.tsx b/src/screens/OrganizationActionItems/ItemViewModal.spec.tsx similarity index 97% rename from src/screens/OrganizationActionItems/ItemViewModal.test.tsx rename to src/screens/OrganizationActionItems/ItemViewModal.spec.tsx index 297cfab6a8..b6423e356d 100644 --- a/src/screens/OrganizationActionItems/ItemViewModal.test.tsx +++ b/src/screens/OrganizationActionItems/ItemViewModal.spec.tsx @@ -18,11 +18,12 @@ import type { InterfaceUserInfo, InterfaceVolunteerGroupInfo, } from 'utils/interfaces'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -86,7 +87,7 @@ const actionItemCategory = { const itemProps: InterfaceViewModalProps[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), item: { _id: 'actionItemId1', assignee: createAssignee(assigneeWithoutImage), @@ -108,7 +109,7 @@ const itemProps: InterfaceViewModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), item: { _id: 'actionItemId2', assignee: createAssignee(assigneeWithImage), @@ -130,7 +131,7 @@ const itemProps: InterfaceViewModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), item: { _id: 'actionItemId2', assignee: null, @@ -152,7 +153,7 @@ const itemProps: InterfaceViewModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), item: { _id: 'actionItemId2', assignee: null, @@ -174,7 +175,7 @@ const itemProps: InterfaceViewModalProps[] = [ }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), item: { _id: 'actionItemId2', assignee: null, diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css b/src/screens/OrganizationDashboard/OrganizationDashboard.module.css deleted file mode 100644 index 3ffe274196..0000000000 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardHeader { - padding: 1.25rem 1rem 1rem 1rem; - border-bottom: 1px solid var(--bs-gray-200); - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.cardHeader .cardTitle { - font-size: 1.2rem; - font-weight: 600; -} - -.cardBody { - min-height: 180px; - padding-top: 0; - max-height: 570px; - overflow-y: scroll; - width: 100%; - max-width: 400px; -} - -.cardBody .emptyContainer { - display: flex; - height: 180px; - justify-content: center; - align-items: center; -} - -.rankings { - aspect-ratio: 1; - border-radius: 50%; - width: 35px; -} diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.spec.tsx similarity index 81% rename from src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx rename to src/screens/OrganizationDashboard/OrganizationDashboard.spec.tsx index 88db2aa737..b7b4e05a37 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.test.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.spec.tsx @@ -7,7 +7,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18n from 'utils/i18nForTest'; @@ -15,11 +15,25 @@ import OrganizationDashboard from './OrganizationDashboard'; import type { ApolloLink } from '@apollo/client'; import { MOCKS, EMPTY_MOCKS, ERROR_MOCKS } from './OrganizationDashboardMocks'; import { toast } from 'react-toastify'; - -jest.mock('react-toastify', () => ({ +import { vi } from 'vitest'; + +/** + * This file contains unit tests for the OrganizationDashboard component. + * + * The tests cover: + * - Behavior when URL parameters are undefined, including redirection to fallback URLs. + * - Rendering of key sections, such as dashboard cards, upcoming events, latest posts, membership requests, and volunteer rankings. + * - Functionality of user interactions with dashboard elements (e.g., navigation via clicks on cards and buttons). + * - Handling of scenarios with empty data or errors in GraphQL responses. + * - Integration with mocked GraphQL queries and toast notifications. + * + * These tests are implemented using Vitest for test execution and MockedProvider for mocking GraphQL queries. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -89,40 +103,48 @@ const renderOrganizationDashboard = (link: ApolloLink): RenderResult => { describe('Testing Organization Dashboard Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + vi.mock('react-router-dom', async () => { + const originalModule = await vi.importActual('react-router-dom'); + return { + ...originalModule, + useParams: vi.fn(), + }; + }); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { + vi.mocked(useParams).mockReturnValue({}); render( - } />
} /> + } /> , ); + await waitFor(() => { + expect(window.location.pathname).toBe('/'); + }); await waitFor(() => { expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); }); it('should render Organization Dashboard screen', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); // Dashboard cards @@ -151,6 +173,7 @@ describe('Testing Organization Dashboard Screen', () => { }); it('Click People Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const membersBtn = await screen.findByText(t.members); expect(membersBtn).toBeInTheDocument(); @@ -162,6 +185,7 @@ describe('Testing Organization Dashboard Screen', () => { }); it('Click Admin Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const adminsBtn = await screen.findByText(t.admins); expect(adminsBtn).toBeInTheDocument(); @@ -169,6 +193,7 @@ describe('Testing Organization Dashboard Screen', () => { }); it('Click Post Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const postsBtn = await screen.findByText(t.posts); expect(postsBtn).toBeInTheDocument(); @@ -180,6 +205,7 @@ it('Click Post Card', async () => { }); it('Click Events Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const eventsBtn = await screen.findByText(t.events); expect(eventsBtn).toBeInTheDocument(); @@ -191,6 +217,7 @@ it('Click Events Card', async () => { }); it('Click Blocked Users Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const blockedUsersBtn = await screen.findByText(t.blockedUsers); expect(blockedUsersBtn).toBeInTheDocument(); @@ -202,6 +229,7 @@ it('Click Blocked Users Card', async () => { }); it('Click Requests Card', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const requestsBtn = await screen.findByText(t.requests); expect(requestsBtn).toBeInTheDocument(); @@ -213,6 +241,7 @@ it('Click Requests Card', async () => { }); it('Click View All Events', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const viewAllBtn = await screen.findAllByText(t.viewAll); expect(viewAllBtn[0]).toBeInTheDocument(); @@ -224,6 +253,7 @@ it('Click View All Events', async () => { }); it('Click View All Posts', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const viewAllBtn = await screen.findAllByText(t.viewAll); expect(viewAllBtn[1]).toBeInTheDocument(); @@ -235,6 +265,7 @@ it('Click View All Posts', async () => { }); it('Click View All Requests', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const viewAllBtn = await screen.findAllByText(t.viewAll); expect(viewAllBtn[2]).toBeInTheDocument(); @@ -246,6 +277,7 @@ it('Click View All Requests', async () => { }); it('Click View All Leaderboard', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link1); const viewAllBtn = await screen.findAllByText(t.viewAll); expect(viewAllBtn[3]).toBeInTheDocument(); @@ -257,6 +289,7 @@ it('Click View All Leaderboard', async () => { }); it('should render Organization Dashboard screen with empty data', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link3); await waitFor(() => { @@ -268,6 +301,7 @@ it('should render Organization Dashboard screen with empty data', async () => { }); it('should redirectt to / if error occurs', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationDashboard(link2); await waitFor(() => { diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx index ebea874d2e..abc712289c 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx @@ -31,7 +31,7 @@ import type { InterfaceQueryOrganizationsListObject, InterfaceVolunteerRank, } from 'utils/interfaces'; -import styles from './OrganizationDashboard.module.css'; +import styles from 'style/app.module.css'; import { VOLUNTEER_RANKING } from 'GraphQl/Queries/EventVolunteerQueries'; /** @@ -41,7 +41,7 @@ import { VOLUNTEER_RANKING } from 'GraphQl/Queries/EventVolunteerQueries'; * * @returns The rendered component. */ -function organizationDashboard(): JSX.Element { +function OrganizationDashboard(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'dashboard' }); const { t: tCommon } = useTranslation('common'); const { t: tErrors } = useTranslation('errors'); @@ -299,7 +299,7 @@ function organizationDashboard(): JSX.Element { {t('viewAll')}
- + {loadingEvent ? ( [...Array(4)].map((_, index) => { return ; @@ -341,7 +341,7 @@ function organizationDashboard(): JSX.Element { {t('viewAll')}
- + {loadingPost ? ( [...Array(4)].map((_, index) => { return ; @@ -392,7 +392,7 @@ function organizationDashboard(): JSX.Element {
{loadingOrgData ? ( @@ -435,7 +435,10 @@ function organizationDashboard(): JSX.Element { {t('viewAll')}
- + {rankingsLoading ? ( [...Array(3)].map((_, index) => { return ; @@ -483,4 +486,4 @@ function organizationDashboard(): JSX.Element { ); } -export default organizationDashboard; +export default OrganizationDashboard; diff --git a/src/screens/OrganizationFunds/FundModal.test.tsx b/src/screens/OrganizationFunds/FundModal.spec.tsx similarity index 85% rename from src/screens/OrganizationFunds/FundModal.test.tsx rename to src/screens/OrganizationFunds/FundModal.spec.tsx index c74b0434c3..7296f31661 100644 --- a/src/screens/OrganizationFunds/FundModal.test.tsx +++ b/src/screens/OrganizationFunds/FundModal.spec.tsx @@ -21,11 +21,12 @@ import { toast } from 'react-toastify'; import { MOCKS, MOCKS_ERROR } from './OrganizationFundsMocks'; import type { InterfaceFundModal } from './FundModal'; import FundModal from './FundModal'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -38,7 +39,7 @@ const translations = JSON.parse( const fundProps: InterfaceFundModal[] = [ { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), fund: { _id: 'fundId', name: 'Fund 1', @@ -54,13 +55,13 @@ const fundProps: InterfaceFundModal[] = [ lastName: 'Doe', }, }, - refetchFunds: jest.fn(), + refetchFunds: vi.fn(), orgId: 'orgId', mode: 'create', }, { isOpen: true, - hide: jest.fn(), + hide: vi.fn(), fund: { _id: 'fundId', name: 'Fund 1', @@ -76,7 +77,7 @@ const fundProps: InterfaceFundModal[] = [ lastName: 'Doe', }, }, - refetchFunds: jest.fn(), + refetchFunds: vi.fn(), orgId: 'orgId', mode: 'edit', }, @@ -104,6 +105,7 @@ const renderFundModal = ( describe('PledgeModal', () => { afterEach(() => { cleanup(); + vi.clearAllMocks(); }); it('should populate form fields with correct values in edit mode', async () => { @@ -184,6 +186,37 @@ describe('PledgeModal', () => { }); }); + it('should not update the fund when no fields are changed', async () => { + renderFundModal(link1, fundProps[1]); + + // Simulate no change to the fields + const fundNameInput = screen.getByLabelText(translations.fundName); + fireEvent.change(fundNameInput, { target: { value: 'Fund 1' } }); + + const fundIdInput = screen.getByLabelText(translations.fundId); + fireEvent.change(fundIdInput, { target: { value: '1111' } }); + + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + fireEvent.click(taxDeductibleSwitch); + fireEvent.click(taxDeductibleSwitch); + + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + fireEvent.click(defaultSwitch); + fireEvent.click(defaultSwitch); + + const archivedSwitch = screen.getByTestId('archivedSwitch'); + fireEvent.click(archivedSwitch); + fireEvent.click(archivedSwitch); + + fireEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + + await waitFor(() => { + expect(toast.success).not.toHaveBeenCalled(); + expect(fundProps[1].refetchFunds).not.toHaveBeenCalled(); + expect(fundProps[1].hide).not.toHaveBeenCalled(); + }); + }); + it('should update fund', async () => { renderFundModal(link1, fundProps[1]); diff --git a/src/screens/OrganizationFunds/FundModal.tsx b/src/screens/OrganizationFunds/FundModal.tsx index de4329e55c..b33270b30f 100644 --- a/src/screens/OrganizationFunds/FundModal.tsx +++ b/src/screens/OrganizationFunds/FundModal.tsx @@ -139,7 +139,9 @@ const FundModal: React.FC = ({ if (isDefault != fund?.isDefault) { updatedFields.isDefault = isDefault; } - + if (Object.keys(updatedFields).length === 0) { + return; + } await updateFund({ variables: { id: fund?._id, @@ -157,9 +159,7 @@ const FundModal: React.FC = ({ hide(); toast.success(t('fundUpdated') as string); } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - } + toast.error((error as Error).message); } }; diff --git a/src/screens/OrganizationFunds/OrganizationFunds.test.tsx b/src/screens/OrganizationFunds/OrganizationFunds.spec.tsx similarity index 85% rename from src/screens/OrganizationFunds/OrganizationFunds.test.tsx rename to src/screens/OrganizationFunds/OrganizationFunds.spec.tsx index c6983e1d6d..b259b7da65 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.test.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.spec.tsx @@ -11,7 +11,7 @@ import { import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; @@ -20,11 +20,12 @@ import { MOCKS, MOCKS_ERROR, NO_FUNDS } from './OrganizationFundsMocks'; import type { ApolloLink } from '@apollo/client'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -67,21 +68,22 @@ const renderOrganizationFunds = (link: ApolloLink): RenderResult => { describe('OrganizationFunds Screen =>', () => { beforeEach(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); - }); - - afterEach(() => { - jest.clearAllMocks(); + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: vi.fn(), + }; + }); }); afterEach(() => { + vi.clearAllMocks(); cleanup(); }); it('should render the Campaign Pledge screen', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); await waitFor(() => { expect(screen.getByTestId('searchByName')).toBeInTheDocument(); @@ -89,29 +91,34 @@ describe('OrganizationFunds Screen =>', () => { }); it('should redirect to fallback URL if URL params are undefined', async () => { + vi.mocked(useParams).mockReturnValue({}); render( - } />
} /> + } />
, ); + await waitFor(() => { + expect(window.location.pathname).toBe('/'); + }); await waitFor(() => { expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); }); it('open and close Create Fund modal', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const createFundBtn = await screen.findByTestId('createFundBtn'); @@ -128,6 +135,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('open and close update fund modal', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); await waitFor(() => { @@ -150,6 +158,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('Search the Funds list by name', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const searchField = await screen.findByTestId('searchByName'); fireEvent.change(searchField, { @@ -163,6 +172,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('should render the Fund screen with error', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link2); await waitFor(() => { expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); @@ -170,6 +180,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('renders the empty fund component', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link3); await waitFor(() => expect(screen.getByText(translations.noFundsFound)).toBeInTheDocument(), @@ -177,6 +188,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('Sort the Pledges list by Latest created Date', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const sortBtn = await screen.findByTestId('filter'); @@ -198,6 +210,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('Sort the Pledges list by Earliest created Date', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const sortBtn = await screen.findByTestId('filter'); @@ -219,6 +232,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('Click on Fund Name', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const fundName = await screen.findAllByTestId('fundName'); @@ -231,6 +245,7 @@ describe('OrganizationFunds Screen =>', () => { }); it('Click on View Campaign', async () => { + vi.mocked(useParams).mockReturnValue({ orgId: 'orgId' }); renderOrganizationFunds(link1); const viewBtn = await screen.findAllByTestId('viewBtn'); diff --git a/src/screens/OrganizationPeople/AddMember.tsx b/src/screens/OrganizationPeople/AddMember.tsx index 750d831abf..c7987b738d 100644 --- a/src/screens/OrganizationPeople/AddMember.tsx +++ b/src/screens/OrganizationPeople/AddMember.tsx @@ -76,6 +76,9 @@ function AddMember(): JSX.Element { function openAddUserModal(): void { setAddUserModalIsOpen(true); } + useEffect(() => { + setUserName(''); + }, [addUserModalisOpen]); const toggleDialogModal = (): void => setAddUserModalIsOpen(!addUserModalisOpen); diff --git a/src/screens/OrganizationPeople/OrganizationPeople.test.tsx b/src/screens/OrganizationPeople/OrganizationPeople.spec.tsx similarity index 95% rename from src/screens/OrganizationPeople/OrganizationPeople.test.tsx rename to src/screens/OrganizationPeople/OrganizationPeople.spec.tsx index a840f1e1f0..1477c646f6 100644 --- a/src/screens/OrganizationPeople/OrganizationPeople.test.tsx +++ b/src/screens/OrganizationPeople/OrganizationPeople.spec.tsx @@ -2,6 +2,7 @@ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; +import type { Params } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; @@ -13,7 +14,7 @@ import { USERS_CONNECTION_LIST, USER_LIST_FOR_TABLE, } from 'GraphQl/Queries/Queries'; -import 'jest-location-mock'; +// import 'jest-location-mock'; import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; import { @@ -21,6 +22,29 @@ import { SIGNUP_MUTATION, } from 'GraphQl/Mutations/mutations'; import type { TestMock } from './MockDataTypes'; +import { vi } from 'vitest'; + +/** + * Mock window.location for testing redirection behavior. + */ + +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost/', + assign: vi.fn((url) => { + const urlObj = new URL(url, 'http://localhost'); + window.location.href = urlObj.href; + window.location.pathname = urlObj.pathname; + window.location.search = urlObj.search; + window.location.hash = urlObj.hash; + }), + reload: vi.fn(), + pathname: '/', + search: '', + hash: '', + origin: 'http://localhost', + }, +}); const createMemberMock = ( orgId = '', @@ -596,14 +620,18 @@ async function wait(ms = 2): Promise { }); } const linkURL = 'orgid'; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: linkURL }), -})); + +vi.mock('react-router-dom', async () => { + const actualDom = await vi.importActual('react-router-dom'); + return { + ...actualDom, + useParams: (): Readonly> => ({ orgId: linkURL }), + }; +}); // TODO - REMOVE THE NEXT LINE IT IS TO SUPPRESS THE ERROR // FOR THE FIRST TEST WHICH CAME OUT OF NOWHERE -console.error = jest.fn(); +console.error = vi.fn(); describe('Organization People Page', () => { const searchData = { @@ -683,7 +711,7 @@ describe('Organization People Page', () => { }, ]); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('It is necessary to query the correct mock data.', async () => { @@ -705,7 +733,7 @@ describe('Organization People Page', () => { await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('Testing MEMBERS list', async () => { @@ -753,7 +781,7 @@ describe('Organization People Page', () => { ); await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('Testing MEMBERS list with filters', async () => { @@ -792,7 +820,7 @@ describe('Organization People Page', () => { await wait(); expect(findtext).toBeInTheDocument(); await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('Testing ADMIN LIST', async () => { @@ -855,7 +883,7 @@ describe('Organization People Page', () => { // Wait for any asynchronous operations to complete await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('Testing ADMIN list with filters', async () => { @@ -905,7 +933,7 @@ describe('Organization People Page', () => { // Ensure that the name is still present after filtering await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); }); test('Testing add existing user modal', async () => { @@ -1256,7 +1284,9 @@ describe('Organization People Page', () => { const btn = screen.getByTestId('searchbtn'); userEvent.click(btn); await wait(); - expect(window.location).toBeAt('/orgpeople/6401ff65ce8e8406b8f07af1'); + expect(window.location.href).toBe( + 'http://localhost/orgpeople/6401ff65ce8e8406b8f07af1', + ); }); test('Testing USERS list with filters', async () => { @@ -1289,7 +1319,9 @@ describe('Organization People Page', () => { const btn = screen.getByTestId('searchbtn'); userEvent.click(btn); await wait(); - expect(window.location).toBeAt('/orgpeople/6401ff65ce8e8406b8f07af2'); + expect(window.location.href).toBe( + 'http://localhost/orgpeople/6401ff65ce8e8406b8f07af2', + ); }); test('Add Member component renders', async () => { @@ -1378,7 +1410,7 @@ describe('Organization People Page', () => { ); await wait(); - expect(window.location).toBeAt('/orgpeople/orgid'); + expect(window.location.href).toBe('http://localhost/orgpeople/orgid'); expect(screen.queryByText(/Nothing Found !!/i)).toBeInTheDocument(); }); }); diff --git a/src/screens/OrganizationVenues/OrganizationVenues.test.tsx b/src/screens/OrganizationVenues/OrganizationVenues.spec.tsx similarity index 92% rename from src/screens/OrganizationVenues/OrganizationVenues.test.tsx rename to src/screens/OrganizationVenues/OrganizationVenues.spec.tsx index 5b8b9933a1..e306c56cfc 100644 --- a/src/screens/OrganizationVenues/OrganizationVenues.test.tsx +++ b/src/screens/OrganizationVenues/OrganizationVenues.spec.tsx @@ -1,3 +1,15 @@ +/** + * Tests for the OrganizationVenues component. + * These tests include: + * - Ensuring the component renders correctly with default props. + * - Handling the absence of `orgId` by redirecting to the homepage. + * - Fetching and displaying venues via Apollo GraphQL queries. + * - Allowing users to search venues by name or description. + * - Sorting venues by capacity in ascending or descending order. + * - Verifying that long venue names or descriptions are handled gracefully. + * - Testing loading states and edge cases for Apollo queries. + * - Mocking GraphQL mutations for venue-related actions and validating their behavior. + */ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import type { RenderResult } from '@testing-library/react'; @@ -10,7 +22,6 @@ import { } from '@testing-library/react'; import { Provider } from 'react-redux'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; import OrganizationVenues from './OrganizationVenues'; import { store } from 'state/store'; @@ -19,7 +30,7 @@ import { StaticMockLink } from 'utils/StaticMockLink'; import { VENUE_LIST } from 'GraphQl/Queries/OrganizationQueries'; import type { ApolloLink } from '@apollo/client'; import { DELETE_VENUE_MUTATION } from 'GraphQl/Mutations/VenueMutations'; - +import { vi } from 'vitest'; const MOCKS = [ { request: { @@ -239,11 +250,11 @@ async function wait(ms = 100): Promise { }); } -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - warning: jest.fn(), - error: jest.fn(), + success: vi.fn(), + warning: vi.fn(), + error: vi.fn(), }, })); @@ -272,14 +283,14 @@ const renderOrganizationVenue = (link: ApolloLink): RenderResult => { describe('OrganizationVenue with missing orgId', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), + vi.doMock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), useParams: () => ({ orgId: undefined }), })); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('Redirect to /orglist when orgId is falsy/undefined', async () => { render( @@ -299,7 +310,6 @@ describe('OrganizationVenue with missing orgId', () => { , ); - await waitFor(() => { const paramsError = screen.getByTestId('paramsError'); expect(paramsError).toBeInTheDocument(); @@ -308,17 +318,17 @@ describe('OrganizationVenue with missing orgId', () => { }); describe('Organisation Venues', () => { - global.alert = jest.fn(); + global.alert = vi.fn(); beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), + vi.doMock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), useParams: () => ({ orgId: 'orgId' }), })); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('searches the venue list correctly by Name', async () => { diff --git a/src/screens/PageNotFound/PageNotFound.module.css b/src/screens/PageNotFound/PageNotFound.module.css deleted file mode 100644 index 3c1b9a3413..0000000000 --- a/src/screens/PageNotFound/PageNotFound.module.css +++ /dev/null @@ -1,109 +0,0 @@ -.notfound { - position: relative; - bottom: 20px; -} - -.notfound h3 { - font-family: 'Roboto', sans-serif; - font-weight: normal; - letter-spacing: 1px; -} - -.notfound .brand span { - margin-top: 50px; - font-size: 40px; -} -.notfound .brand h3 { - font-weight: 300; - margin: 10px 0 0 0; -} -.notfound h1.head { - font-size: 250px; - font-weight: 900; - color: #31bb6b; - letter-spacing: 25px; - margin: 10px 0 0 0; -} -.notfound h1.head span { - position: relative; - display: inline-block; -} -.notfound h1.head span:before, -.notfound h1.head span:after { - position: absolute; - top: 50%; - width: 50%; - height: 1px; - background: #fff; - content: ''; -} -.notfound h1.head span:before { - left: -55%; -} -.notfound h1.head span:after { - right: -55%; -} - -@media (max-width: 1024px) { - .notfound h1.head { - font-size: 200px; - letter-spacing: 25px; - } -} - -@media (max-width: 768px) { - .notfound h1.head { - font-size: 150px; - letter-spacing: 25px; - } -} - -@media (max-width: 640px) { - .notfound h1.head { - font-size: 150px; - letter-spacing: 0; - } -} - -@media (max-width: 480px) { - .notfound .brand h3 { - font-size: 20px; - } - .notfound h1.head { - font-size: 130px; - letter-spacing: 0; - } - .notfound h1.head span:before, - .notfound h1.head span:after { - width: 40%; - } - .notfound h1.head span:before { - left: -45%; - } - .notfound h1.head span:after { - right: -45%; - } - .notfound p { - font-size: 18px; - } -} - -@media (max-width: 320px) { - .notfound .brand h3 { - font-size: 16px; - } - .notfound h1.head { - font-size: 100px; - letter-spacing: 0; - } - .notfound h1.head span:before, - .notfound h1.head span:after { - width: 25%; - } - .notfound h1.head span:before { - left: -30%; - } - .notfound h1.head span:after { - right: -30%; - } -} diff --git a/src/screens/PageNotFound/PageNotFound.test.tsx b/src/screens/PageNotFound/PageNotFound.spec.tsx similarity index 68% rename from src/screens/PageNotFound/PageNotFound.test.tsx rename to src/screens/PageNotFound/PageNotFound.spec.tsx index 501d9f7ef3..4557eb5af4 100644 --- a/src/screens/PageNotFound/PageNotFound.test.tsx +++ b/src/screens/PageNotFound/PageNotFound.spec.tsx @@ -8,11 +8,11 @@ import { store } from 'state/store'; import PageNotFound from './PageNotFound'; import i18nForTest from 'utils/i18nForTest'; import useLocalStorage from 'utils/useLocalstorage'; - +import { it, expect, describe } from 'vitest'; const { setItem } = useLocalStorage(); describe('Testing Page not found component', () => { - test('Component should be rendered properly for User', () => { + it('should render component properly for User', () => { //setItem('AdminFor', undefined); render( @@ -24,15 +24,15 @@ describe('Testing Page not found component', () => { , ); - expect(screen.getByText(/Talawa User/i)).toBeTruthy(); - expect(screen.getByText(/404/i)).toBeTruthy(); + expect(screen.getByText(/Talawa User/i)).toBeInTheDocument(); + expect(screen.getByText(/404/i)).toBeInTheDocument(); expect( screen.getByText(/Oops! The Page you requested was not found!/i), - ).toBeTruthy(); - expect(screen.getByText(/Back to Home/i)).toBeTruthy(); + ).toBeInTheDocument(); + expect(screen.getByText(/Back to Home/i)).toBeInTheDocument(); }); - test('Component should be rendered properly for ADMIN or SUPERADMIN', () => { + it('should render properly for ADMIN or SUPERADMIN', () => { setItem('AdminFor', [ { _id: '6537904485008f171cf29924', @@ -49,11 +49,11 @@ describe('Testing Page not found component', () => { , ); - expect(screen.getByText(/Talawa Admin Portal/i)).toBeTruthy(); - expect(screen.getByText(/404/i)).toBeTruthy(); + expect(screen.getByText(/Talawa Admin Portal/i)).toBeInTheDocument(); + expect(screen.getByText(/404/i)).toBeInTheDocument(); expect( screen.getByText(/Oops! The Page you requested was not found!/i), - ).toBeTruthy(); - expect(screen.getByText(/Back to Home/i)).toBeTruthy(); + ).toBeInTheDocument(); + expect(screen.getByText(/Back to Home/i)).toBeInTheDocument(); }); }); diff --git a/src/screens/PageNotFound/PageNotFound.tsx b/src/screens/PageNotFound/PageNotFound.tsx index 037aeecbab..62ed90c423 100644 --- a/src/screens/PageNotFound/PageNotFound.tsx +++ b/src/screens/PageNotFound/PageNotFound.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import useLocalStorage from 'utils/useLocalstorage'; -import styles from './PageNotFound.module.css'; +import styles from '../../style/app.module.css'; import Logo from 'assets/images/talawa-logo-600x600.png'; /** @@ -28,7 +28,7 @@ const PageNotFound = (): JSX.Element => { const adminFor = getItem('AdminFor'); return ( -
+
Logo diff --git a/src/screens/Requests/Requests.test.tsx b/src/screens/Requests/Requests.spec.tsx similarity index 91% rename from src/screens/Requests/Requests.test.tsx rename to src/screens/Requests/Requests.spec.tsx index 4606fdae08..820b24c40d 100644 --- a/src/screens/Requests/Requests.test.tsx +++ b/src/screens/Requests/Requests.spec.tsx @@ -1,8 +1,6 @@ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { render, screen } from '@testing-library/react'; -import 'jest-localstorage-mock'; -import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -22,6 +20,34 @@ import { MOCKS4, } from './RequestsMocks'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi } from 'vitest'; + +/** + * Set up `localStorage` stubs for testing. + */ + +vi.stubGlobal('localStorage', { + getItem: vi.fn(), + setItem: vi.fn(), + clear: vi.fn(), + removeItem: vi.fn(), +}); + +/** + * Mock `window.location` for testing redirection behavior. + */ + +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost/', + assign: vi.fn(), + reload: vi.fn(), + pathname: '/', + search: '', + hash: '', + origin: 'http://localhost', + }, +}); const { setItem, removeItem } = useLocalStorage(); @@ -33,6 +59,14 @@ const link5 = new StaticMockLink(MOCKS_WITH_ERROR, true); const link6 = new StaticMockLink(MOCKS3, true); const link7 = new StaticMockLink(MOCKS4, true); +/** + * Utility function to wait for a specified amount of time. + * Wraps `setTimeout` in an `act` block for testing purposes. + * + * @param ms - The duration to wait in milliseconds. Default is 100ms. + * @returns A promise that resolves after the specified time. + */ + async function wait(ms = 100): Promise { await act(() => { return new Promise((resolve) => { @@ -53,7 +87,6 @@ afterEach(() => { describe('Testing Requests screen', () => { test('Component should be rendered properly', async () => { - const loadMoreRequests = jest.fn(); render( diff --git a/src/screens/SubTags/SubTags.module.css b/src/screens/SubTags/SubTags.module.css deleted file mode 100644 index 0a210bdfa4..0000000000 --- a/src/screens/SubTags/SubTags.module.css +++ /dev/null @@ -1,145 +0,0 @@ -.btnsContainer { - display: flex; - margin: 2rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; - width: max-content; -} - -.btnsContainer .btnsBlock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainer .input { - flex: 1; - position: relative; - max-width: 60%; - justify-content: space-between; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .input button { - width: 52px; -} - -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock button { - margin: 0; - } - - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; - } -} - -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} - -.errorContainer { - min-height: 100vh; -} - -.errorMessage { - margin-top: 25%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -.errorIcon { - transform: scale(1.5); - color: var(--bs-danger); - margin-bottom: 1rem; -} - -.tableHeader { - background-color: var(--bs-primary); - color: var(--bs-white); - font-size: 1rem; -} -.rowBackground { - background-color: var(--bs-white); - max-height: 120px; -} - -.subTagsLink { - color: var(--bs-blue); - font-weight: 500; - cursor: pointer; -} - -.subTagsLink i { - visibility: hidden; -} - -.subTagsLink:hover { - font-weight: 600; - text-decoration: underline; -} - -.subTagsLink:hover i { - visibility: visible; -} - -.tagsBreadCrumbs { - color: var(--bs-gray); - cursor: pointer; -} - -.tagsBreadCrumbs:hover { - color: var(--bs-blue); - font-weight: 600; - text-decoration: underline; -} - -.subTagsScrollableDiv { - scrollbar-width: auto; - scrollbar-color: var(--bs-gray-400) var(--bs-white); - - max-height: calc(100vh - 18rem); - overflow: auto; -} diff --git a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx b/src/screens/UserPortal/Campaigns/Campaigns.spec.tsx similarity index 88% rename from src/screens/UserPortal/Campaigns/Campaigns.test.tsx rename to src/screens/UserPortal/Campaigns/Campaigns.spec.tsx index 17b7eec4d5..09cde6b25d 100644 --- a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx +++ b/src/screens/UserPortal/Campaigns/Campaigns.spec.tsx @@ -20,39 +20,57 @@ import i18nForTest from 'utils/i18nForTest'; import type { ApolloLink } from '@apollo/client'; import useLocalStorage from 'utils/useLocalstorage'; import Campaigns from './Campaigns'; +import { vi, it, expect, describe } from 'vitest'; import { EMPTY_MOCKS, MOCKS, USER_FUND_CAMPAIGNS_ERROR, } from './CampaignsMocks'; -jest.mock('react-toastify', () => ({ +/* Mocking 'react-toastify` */ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { + +/* Mocking `@mui/x-date-pickers/DateTimePicker` */ +vi.mock('@mui/x-date-pickers/DateTimePicker', async () => { + const actual = await vi.importActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ); return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, + DateTimePicker: actual.DesktopDateTimePicker, }; }); + const { setItem } = useLocalStorage(); +/** + * Creates a mocked Apollo link for testing. + */ const link1 = new StaticMockLink(MOCKS); const link2 = new StaticMockLink(USER_FUND_CAMPAIGNS_ERROR); const link3 = new StaticMockLink(EMPTY_MOCKS); + const cTranslations = JSON.parse( JSON.stringify( i18nForTest.getDataByLanguage('en')?.translation.userCampaigns, ), ); + const pTranslations = JSON.parse( JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.pledges), ); +/* + * Renders the `Campaigns` component for testing. + * + * @param link - The mocked Apollo link used for testing. + * @returns The rendered result of the `Campaigns` component. + */ + const renderCampaigns = (link: ApolloLink): RenderResult => { return render( @@ -79,26 +97,38 @@ const renderCampaigns = (link: ApolloLink): RenderResult => { ); }; +/** + * Test suite for the User Campaigns screen. + */ describe('Testing User Campaigns Screen', () => { beforeEach(() => { setItem('userId', 'userId'); }); beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + /** + * Mocks the `useParams` function from `react-router-dom` to simulate URL parameters. + */ + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: vi.fn(() => ({ orgId: 'orgId' })), // Mock `useParams` + }; + }); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterEach(() => { cleanup(); }); + /** + * Verifies that the User Campaigns screen renders correctly with mock data. + */ it('should render the User Campaigns screen', async () => { renderCampaigns(link1); await waitFor(() => { @@ -108,6 +138,9 @@ describe('Testing User Campaigns Screen', () => { }); }); + /** + * Ensures the app redirects to the fallback URL if `userId` is null in LocalStorage. + */ it('should redirect to fallback URL if userId is null in LocalStorage', async () => { setItem('userId', null); renderCampaigns(link1); @@ -116,7 +149,11 @@ describe('Testing User Campaigns Screen', () => { }); }); + /** + * Ensures the app redirects to the fallback URL if URL parameters are undefined. + */ it('should redirect to fallback URL if URL params are undefined', async () => { + vi.unmock('react-router-dom'); // unmocking to get real behavior from useParams render( diff --git a/src/screens/UserPortal/Chat/Chat.test.tsx b/src/screens/UserPortal/Chat/Chat.spec.tsx similarity index 94% rename from src/screens/UserPortal/Chat/Chat.test.tsx rename to src/screens/UserPortal/Chat/Chat.spec.tsx index 660eeaa236..ec8243c823 100644 --- a/src/screens/UserPortal/Chat/Chat.test.tsx +++ b/src/screens/UserPortal/Chat/Chat.spec.tsx @@ -1,27 +1,55 @@ import React from 'react'; import { - act, - fireEvent, render, + fireEvent, screen, waitFor, + act, } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; -import { I18nextProvider } from 'react-i18next'; - -import { - USERS_CONNECTION_LIST, - USER_JOINED_ORGANIZATIONS, -} from 'GraphQl/Queries/Queries'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; +import { expect, describe, test, vi } from 'vitest'; import { store } from 'state/store'; import i18nForTest from 'utils/i18nForTest'; +import { I18nextProvider } from 'react-i18next'; import Chat from './Chat'; -import useLocalStorage from 'utils/useLocalstorage'; +import { + USERS_CONNECTION_LIST, + USER_JOINED_ORGANIZATIONS, +} from 'GraphQl/Queries/Queries'; import { MESSAGE_SENT_TO_CHAT } from 'GraphQl/Mutations/OrganizationMutations'; import { CHATS_LIST, CHAT_BY_ID } from 'GraphQl/Queries/PlugInQueries'; +import useLocalStorage from 'utils/useLocalstorage'; +/** + * Unit tests for the ChatScreen component. + * These tests covers all areas + * Covers: + * - Rendering of UI elements. + * - Contact selection functionality. + * - Direct and group chat creation workflows. + * - Sidebar behavior in desktop and mobile views. + * + * Uses Vitest for testing and Apollo MockedProvider for mocking GraphQL queries. + */ + +vi.mock('../../../components/UserPortal/ChatRoom/ChatRoom', () => ({ + default: () =>
Mocked ChatRoom
, +})); + +const resizeWindow = (width: number): void => { + window.innerWidth = width; + fireEvent(window, new Event('resize')); +}; + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} const { setItem } = useLocalStorage(); const USER_JOINED_ORG_MOCK = [ @@ -1457,47 +1485,45 @@ const GROUP_CHAT_BY_ID_QUERY_MOCK = [ }, ]; -const resizeWindow = (width: number): void => { - window.innerWidth = width; - fireEvent(window, new Event('resize')); -}; - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - describe('Testing Chat Screen [User Portal]', () => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); - test('Screen should be rendered properly', async () => { - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...UserConnectionListMock, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; + // Define mock data outside of tests to reuse + const mock = [ + ...USER_JOINED_ORG_MOCK, + ...GROUP_CHAT_BY_ID_QUERY_MOCK, + ...MESSAGE_SENT_TO_CHAT_MOCK, + ...MESSAGE_SENT_TO_CHAT_MOCK, + ...UserConnectionListMock, + ...CHAT_BY_ID_QUERY_MOCK, + ...CHATS_LIST_MOCK, + ...UserConnectionListMock, + ]; + + beforeEach(() => { + setItem('userId', '1'); + vi.clearAllMocks(); + localStorage.clear(); + vi.resetModules(); + }); + afterEach(() => { + localStorage.clear(); + }); + + test('Screen should be rendered properly', async () => { render( @@ -1512,18 +1538,7 @@ describe('Testing Chat Screen [User Portal]', () => { await wait(); }); - test('User is able to select a contact', async () => { - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...UserConnectionListMock, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; - + it('User is able to select a contact', async () => { render( @@ -1537,24 +1552,13 @@ describe('Testing Chat Screen [User Portal]', () => { ); await wait(); - expect(await screen.findByText('Messages')).toBeInTheDocument(); - expect( await screen.findByTestId('contactCardContainer'), ).toBeInTheDocument(); }); test('create new direct chat', async () => { - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; render( @@ -1572,6 +1576,7 @@ describe('Testing Chat Screen [User Portal]', () => { const dropdown = await screen.findByTestId('dropdown'); expect(dropdown).toBeInTheDocument(); fireEvent.click(dropdown); + const newDirectChatBtn = await screen.findByTestId('newDirectChat'); expect(newDirectChatBtn).toBeInTheDocument(); fireEvent.click(newDirectChatBtn); @@ -1586,16 +1591,6 @@ describe('Testing Chat Screen [User Portal]', () => { }); test('create new group chat', async () => { - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...UserConnectionListMock, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; render( @@ -1618,6 +1613,7 @@ describe('Testing Chat Screen [User Portal]', () => { expect(newGroupChatBtn).toBeInTheDocument(); fireEvent.click(newGroupChatBtn); + const closeButton = screen.getByRole('button', { name: /close/i }); expect(closeButton).toBeInTheDocument(); @@ -1625,18 +1621,6 @@ describe('Testing Chat Screen [User Portal]', () => { }); test('sidebar', async () => { - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...UserConnectionListMock, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; - setItem('userId', '1'); - render( @@ -1661,18 +1645,9 @@ describe('Testing Chat Screen [User Portal]', () => { }); test('Testing sidebar when the screen size is less than or equal to 820px', async () => { - setItem('userId', '1'); - const mock = [ - ...USER_JOINED_ORG_MOCK, - ...GROUP_CHAT_BY_ID_QUERY_MOCK, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...UserConnectionListMock, - ...MESSAGE_SENT_TO_CHAT_MOCK, - ...CHAT_BY_ID_QUERY_MOCK, - ...CHATS_LIST_MOCK, - ...UserConnectionListMock, - ]; + // Resize window for mobile view (<= 820px) resizeWindow(800); + render( @@ -1684,12 +1659,17 @@ describe('Testing Chat Screen [User Portal]', () => { , ); - await wait(); - expect(screen.getByText('My Organizations')).toBeInTheDocument(); - expect(screen.getByText('Talawa User Portal')).toBeInTheDocument(); - expect(await screen.findByTestId('openMenu')).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('openMenu')); - expect(await screen.findByTestId('closeMenu')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('My Organizations')).toBeInTheDocument(); + expect(screen.getByText('Talawa User Portal')).toBeInTheDocument(); + }); + + const openMenuBtn = await screen.findByTestId('openMenu'); + expect(openMenuBtn).toBeInTheDocument(); + fireEvent.click(openMenuBtn); + + const closeMenuBtn = await screen.findByTestId('closeMenu'); + expect(closeMenuBtn).toBeInTheDocument(); }); }); diff --git a/src/screens/UserPortal/Donate/Donate.test.tsx b/src/screens/UserPortal/Donate/Donate.spec.tsx similarity index 93% rename from src/screens/UserPortal/Donate/Donate.test.tsx rename to src/screens/UserPortal/Donate/Donate.spec.tsx index c4d435415e..b13056f5f9 100644 --- a/src/screens/UserPortal/Donate/Donate.test.tsx +++ b/src/screens/UserPortal/Donate/Donate.spec.tsx @@ -1,8 +1,14 @@ +/** + * Unit tests for the Donate component. + * + * This file contains tests for the Donate component to ensure it behaves as expected + * under various scenarios. + */ import React, { act } from 'react'; import { render, screen } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; import { I18nextProvider } from 'react-i18next'; - +import { vi } from 'vitest'; import { ORGANIZATION_DONATION_CONNECTION_LIST, USER_ORGANIZATION_CONNECTION, @@ -132,35 +138,35 @@ async function wait(ms = 100): Promise { }); } -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: '' }), +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useParams: vi.fn(() => ({ orgId: '' })), })); -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - error: jest.fn(), - success: jest.fn(), + error: vi.fn(), + success: vi.fn(), }, })); describe('Testing Donate Screen [User Portal]', () => { Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('Screen should be rendered properly', async () => { diff --git a/src/screens/UserPortal/Events/Events.test.tsx b/src/screens/UserPortal/Events/Events.spec.tsx similarity index 83% rename from src/screens/UserPortal/Events/Events.test.tsx rename to src/screens/UserPortal/Events/Events.spec.tsx index 8c0b7c6912..646f5052f0 100644 --- a/src/screens/UserPortal/Events/Events.test.tsx +++ b/src/screens/UserPortal/Events/Events.spec.tsx @@ -1,5 +1,5 @@ import React, { act } from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; import { I18nextProvider } from 'react-i18next'; @@ -19,35 +19,51 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { ThemeProvider } from 'react-bootstrap'; import { createTheme } from '@mui/material'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi } from 'vitest'; -const { setItem, getItem } = useLocalStorage(); +/** + * Unit tests for the Events component. + * + * This file contains tests to verify the functionality and behavior of the Events component + * under various scenarios, including successful event creation, date/time picker handling, + * calendar view toggling, and error handling. Mocked dependencies are used to ensure isolated testing. + */ -jest.mock('react-toastify', () => ({ +const { setItem } = useLocalStorage(); + +vi.mock('react-toastify', () => ({ toast: { - error: jest.fn(), - info: jest.fn(), - success: jest.fn(), + error: vi.fn(), + info: vi.fn(), + success: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DatePicker', () => { +vi.mock('@mui/x-date-pickers/DatePicker', async () => { + const desktopDatePickerModule = await vi.importActual( + '@mui/x-date-pickers/DesktopDatePicker', + ); return { - DatePicker: jest.requireActual('@mui/x-date-pickers/DesktopDatePicker') - .DesktopDatePicker, + DatePicker: desktopDatePickerModule.DesktopDatePicker, }; }); -jest.mock('@mui/x-date-pickers/TimePicker', () => { +vi.mock('@mui/x-date-pickers/TimePicker', async () => { + const timePickerModule = await vi.importActual( + '@mui/x-date-pickers/DesktopTimePicker', + ); return { - TimePicker: jest.requireActual('@mui/x-date-pickers/DesktopTimePicker') - .DesktopTimePicker, + TimePicker: timePickerModule.DesktopTimePicker, }; }); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: '' }), -})); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: '' }), + }; +}); const theme = createTheme({ palette: { @@ -252,19 +268,19 @@ async function wait(ms = 100): Promise { describe('Testing Events Screen [User Portal]', () => { Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); - test('Screen should be rendered properly', async () => { + it('Screen should be rendered properly', async () => { render( @@ -283,7 +299,7 @@ describe('Testing Events Screen [User Portal]', () => { await wait(); }); - test('Create event works as expected when event is not an all day event.', async () => { + it('Create event works as expected when event is not an all day event.', async () => { render( @@ -341,7 +357,7 @@ describe('Testing Events Screen [User Portal]', () => { ); }); - test('Create event works as expected when event is an all day event.', async () => { + it('Create event works as expected when event is an all day event.', async () => { render( @@ -384,7 +400,7 @@ describe('Testing Events Screen [User Portal]', () => { ); }); - test('Switch to calendar view works as expected.', async () => { + it('Switch to calendar view works as expected.', async () => { render( @@ -413,7 +429,7 @@ describe('Testing Events Screen [User Portal]', () => { expect(screen.getByText('Sun')).toBeInTheDocument(); }); - test('Testing DatePicker and TimePicker', async () => { + it('Testing DatePicker and TimePicker', async () => { render( @@ -430,31 +446,39 @@ describe('Testing Events Screen [User Portal]', () => { , ); - await wait(); - - const startDate = '03/23/2024'; - const endDate = '04/23/2024'; - const startTime = '02:00 PM'; - const endTime = '06:00 PM'; - userEvent.click(screen.getByTestId('createEventModalBtn')); + // MM/DD/YYYY + const startDate = new Date(); + const endDate = new Date(); + const startTime = '08:00 AM'; + const endTime = '10:00 AM'; + + await waitFor(() => { + expect(screen.getByLabelText('Start Date')).toBeInTheDocument(); + expect(screen.getByLabelText('End Date')).toBeInTheDocument(); + }); expect(endDate).not.toBeNull(); const endDateDatePicker = screen.getByLabelText('End Date'); expect(startDate).not.toBeNull(); const startDateDatePicker = screen.getByLabelText('Start Date'); + const startDateDayjs = dayjs(startDate); + const endDateDayjs = dayjs(endDate); + fireEvent.change(startDateDatePicker, { - target: { value: startDate }, + target: { value: startDateDayjs.format('MM/DD/YYYY') }, }); fireEvent.change(endDateDatePicker, { - target: { value: endDate }, + target: { value: endDateDayjs.format('MM/DD/YYYY') }, }); - await wait(); - - expect(endDateDatePicker).toHaveValue(endDate); - expect(startDateDatePicker).toHaveValue(startDate); + await waitFor(() => { + expect(startDateDatePicker).toHaveValue( + startDateDayjs.format('MM/DD/YYYY'), + ); + expect(endDateDatePicker).toHaveValue(endDateDayjs.format('MM/DD/YYYY')); + }); userEvent.click(screen.getByTestId('allDayEventCheck')); @@ -475,7 +499,7 @@ describe('Testing Events Screen [User Portal]', () => { expect(startTimePicker).toHaveValue(startTime); }); - test('EndDate null', async () => { + it('EndDate null', async () => { render( diff --git a/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.module.css b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.module.css new file mode 100644 index 0000000000..428d41e271 --- /dev/null +++ b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.module.css @@ -0,0 +1,27 @@ +.modal-dialog { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.modal-content { + width: 100%; + max-width: 500px; +} + +.modal-body { + text-align: center; +} + +.modal-header, +.modal-footer { + justify-content: center; + text-align: center; +} + +.confirmation { + text-align: center; + margin-top: 30px; +} diff --git a/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.test.tsx b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.test.tsx new file mode 100644 index 0000000000..10a889f26b --- /dev/null +++ b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.test.tsx @@ -0,0 +1,519 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { + BrowserRouter, + MemoryRouter, + Route, + Routes, + useNavigate, + useParams, +} from 'react-router-dom'; +import LeaveOrganization from './LeaveOrganization'; +import { + ORGANIZATIONS_LIST, + USER_ORGANIZATION_CONNECTION, +} from 'GraphQl/Queries/Queries'; +import { REMOVE_MEMBER_MUTATION } from 'GraphQl/Mutations/mutations'; +import { getItem } from 'utils/useLocalstorage'; +import { toast } from 'react-toastify'; + +jest.mock('react-toastify', () => ({ + toast: { success: jest.fn() }, // Mock toast function +})); + +Object.defineProperty(window, 'localStorage', { + value: { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), + }, + writable: true, +}); + +// Mock useParams to return a test organization ID +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), +})); + +// Mock the custom hook +jest.mock('utils/useLocalstorage', () => { + return { + getItem: jest.fn((prefix: string, key: string) => { + if (prefix === 'Talawa-admin' && key === 'email') + return 'test@example.com'; + if (prefix === 'Talawa-admin' && key === 'userId') return '12345'; + if (prefix === 'Talawa-admin-error' && key === 'user-email-error') + throw new Error(); + return null; + }), + }; +}); + +// Define mock data +const mocks = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { id: 'test-org-id' }, + }, + result: { + data: { + organizations: [ + { + _id: 'test-org-id', + image: 'https://example.com/organization-image.png', + creator: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + }, + name: 'Test Organization', + description: 'This is a test organization.', + address: { + city: 'New York', + countryCode: 'US', + dependentLocality: null, + line1: '123 Main Street', + line2: 'Suite 456', + postalCode: '10001', + sortingCode: null, + state: 'NY', + }, + userRegistrationRequired: true, + visibleInSearch: true, + members: [ + { + _id: 'member-001', + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + }, + { + _id: 'member-002', + firstName: 'Bob', + lastName: 'Johnson', + email: 'bob.johnson@example.com', + }, + ], + admins: [ + { + _id: 'admin-001', + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + createdAt: '2023-01-15T10:00:00Z', + }, + { + _id: 'admin-002', + firstName: 'Tom', + lastName: 'Wilson', + email: 'tom.wilson@example.com', + createdAt: '2023-02-10T12:30:00Z', + }, + ], + membershipRequests: [ + { + _id: 'req-001', + user: { + firstName: 'Emily', + lastName: 'Brown', + email: 'emily.brown@example.com', + }, + }, + ], + blockedUsers: [ + { + _id: 'blocked-001', + firstName: 'Henry', + lastName: 'Clark', + email: 'henry.clark@example.com', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: REMOVE_MEMBER_MUTATION, + variables: { orgid: 'test-org-id', userid: '12345' }, + }, + result: { + data: { + removeMember: { + _id: 'test-org-id', + success: true, + }, + }, + }, + }, + { + request: { + query: USER_ORGANIZATION_CONNECTION, + variables: { id: 'test-org-id' }, + }, + result: { + data: { + organizationsConnection: [ + { + _id: 'org123', + name: 'Tech Enthusiasts Club', + image: 'https://example.com/org-logo.png', + description: + 'A community of tech lovers who meet to share ideas and projects.', + userRegistrationRequired: true, + creator: { + firstName: 'Alice', + lastName: 'Johnson', + }, + members: [ + { _id: 'user001' }, + { _id: 'user002' }, + { _id: 'user003' }, + ], + admins: [{ _id: 'admin001' }, { _id: 'admin002' }], + createdAt: '2024-01-15T12:34:56.789Z', + address: { + city: 'San Francisco', + countryCode: 'US', + dependentLocality: null, + line1: '123 Tech Ave', + line2: 'Suite 100', + postalCode: '94105', + sortingCode: null, + state: 'California', + }, + membershipRequests: [ + { + _id: 'req001', + user: { + _id: 'user004', + }, + }, + { + _id: 'req002', + user: { + _id: 'user005', + }, + }, + ], + }, + ], + }, + }, + }, +]; + +const errorMocks = [ + { + request: { + query: ORGANIZATIONS_LIST, + variables: { id: 'test-org-id' }, + }, + error: new Error('Failed to load organization details'), + }, + { + request: { + query: REMOVE_MEMBER_MUTATION, + variables: { orgid: 'test-org-id', userid: '12345' }, + }, + error: new Error('Failed to leave organization'), + }, + { + request: { + query: USER_ORGANIZATION_CONNECTION, + variables: { id: 'test-org-id' }, + }, + error: new Error('Operation Failed'), + }, +]; + +beforeEach(() => { + localStorage.clear(); + jest.clearAllMocks(); // Clear mocks before each test + (useParams as jest.Mock).mockReturnValue({ orgId: 'test-org-id' }); +}); + +describe('LeaveOrganization Component', () => { + test('renders organization details and shows loading spinner', async () => { + render( + + + + + , + ); + const spinner = await screen.findByRole('status'); + expect(spinner).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Test Organization')).toBeInTheDocument(); + expect( + screen.getByText('This is a test organization.'), + ).toBeInTheDocument(); + }); + }); + + test('renders organization details and displays content correctly', async () => { + render( + + + + } + /> + + + , + ); + await waitFor(() => { + expect(screen.getByText('Test Organization')).toBeInTheDocument(); + expect( + screen.getByText('This is a test organization.'), + ).toBeInTheDocument(); + }); + }); + + test('shows error message when mutation fails', async () => { + render( + + + + } + /> + + + , + ); + await waitFor(() => { + expect( + screen.queryByText('Loading organization details...'), + ).not.toBeInTheDocument(); + }); + expect(await screen.findByText('Test Organization')).toBeInTheDocument(); + expect( + screen.getByText('This is a test organization.'), + ).toBeInTheDocument(); + const leaveButton = await screen.findByRole('button', { + name: 'Leave Organization', + }); + fireEvent.click(leaveButton); + expect(screen.queryByText(/An error occurred!/i)).not.toBeInTheDocument(); + }); + + test('logs an error when unable to access localStorage', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const userEmail = (() => { + try { + return getItem('Talawa-admin-error', 'user-email-error') ?? ''; + } catch (e) { + console.error('Failed to access localStorage:', e); + return ''; + } + })(); + const userId = (() => { + try { + return getItem('Talawa-admin-error', 'user-email-error') ?? ''; + } catch (e) { + console.error('Failed to access localStorage:', e); + return ''; + } + })(); + expect(userEmail).toBe(''); + expect(userId).toBe(''); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to access localStorage:', + expect.any(Error), + ); + consoleErrorSpy.mockRestore(); + }); + + test('navigates and shows toast when email matches', async () => { + const mockNavigate = jest.fn(); + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + const toastSuccessMock = jest.fn(); + toast.success = toastSuccessMock; + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + await waitFor(() => + expect( + screen.getByText(/Are you sure you want to leave this organization?/i), + ).toBeInTheDocument(), + ); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + await screen.findByText('Continue'); + fireEvent.click(screen.getByText('Continue')); + const emailInput = screen.getByPlaceholderText(/Enter your email/i); + fireEvent.change(emailInput, { + target: { value: 'test@example.com' }, + }); + fireEvent.keyDown(emailInput, { key: 'Enter', code: 'Enter' }); + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith(`/user/organizations`); + }); + await waitFor(() => { + expect(toastSuccessMock).toHaveBeenCalledWith( + 'You have successfully left the organization!', + ); + }); + }); + + test('shows error when email is missing', async () => { + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + await waitFor(() => + expect( + screen.getByText(/Are you sure you want to leave this organization?/i), + ).toBeInTheDocument(), + ); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + await screen.findByText('Continue'); + fireEvent.click(screen.getByText('Continue')); + fireEvent.change(screen.getByPlaceholderText(/Enter your email/i), { + target: { value: '' }, + }); + fireEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect( + screen.getByText('Verification failed: Email does not match.'), + ).toBeInTheDocument(); + }); + }); + + test('shows error when email does not match', async () => { + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + await waitFor(() => + expect( + screen.getByText(/Are you sure you want to leave this organization?/i), + ).toBeInTheDocument(), + ); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + await screen.findByText('Continue'); + fireEvent.click(screen.getByText('Continue')); + fireEvent.change(screen.getByPlaceholderText(/Enter your email/i), { + target: { value: 'different@example.com' }, + }); + fireEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect( + screen.getByText('Verification failed: Email does not match.'), + ).toBeInTheDocument(); + }); + }); + + test('resets state when back button pressed', async () => { + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + await waitFor(() => + expect( + screen.getByText(/Are you sure you want to leave this organization?/i), + ).toBeInTheDocument(), + ); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + await screen.findByText('Continue'); + fireEvent.click(screen.getByText('Continue')); + const closeButton = screen.getByRole('button', { name: /Back/i }); + fireEvent.click(closeButton); + expect( + screen.queryByText(/Are you sure you want to leave this organization?/i), + ).toBeInTheDocument(); + }); + + test('resets state when modal is closed', async () => { + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + const closeButton = screen.getByRole('button', { name: /Cancel/i }); + fireEvent.click(closeButton); + expect(screen.queryByText(/Leave Organization/i)).toBeInTheDocument(); + }); + + test('closes modal and resets state when Esc key is pressed', async () => { + const mockNavigate = jest.fn(); + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + render( + + + + + , + ); + const leaveButton = await screen.findByRole('button', { + name: /Leave Organization/i, + }); + fireEvent.click(leaveButton); + const modal = await screen.findByTestId('leave-organization-modal'); + expect(modal).toBeInTheDocument(); + fireEvent.keyDown(modal, { key: 'Escape', code: 'Escape' }); + await waitFor(() => { + expect(screen.queryByTestId('leave-organization-modal')).toBeNull(); // Modal should no longer be present + }); + expect(modal).not.toBeInTheDocument(); + }); + + test('displays an error alert when query fails', async () => { + render( + + + , + ); + const errorAlert = await screen.findByRole('alert'); + expect(errorAlert).toHaveTextContent(/Error:/i); + }); +}); diff --git a/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.tsx b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.tsx new file mode 100644 index 0000000000..91dc3a54bf --- /dev/null +++ b/src/screens/UserPortal/LeaveOrganization/LeaveOrganization.tsx @@ -0,0 +1,248 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation } from '@apollo/client'; +import { + ORGANIZATIONS_LIST, + USER_ORGANIZATION_CONNECTION, +} from 'GraphQl/Queries/Queries'; +import { REMOVE_MEMBER_MUTATION } from 'GraphQl/Mutations/mutations'; +import { Button, Modal, Form, Spinner, Alert } from 'react-bootstrap'; +import { useParams, useNavigate } from 'react-router-dom'; +import { getItem } from 'utils/useLocalstorage'; +import { toast } from 'react-toastify'; + +const userEmail = (() => { + try { + return getItem('Talawa-admin', 'email') ?? ''; + } catch (e) { + console.error('Failed to access localStorage:', e); + return ''; + } +})(); +const userId = (() => { + try { + return getItem('Talawa-admin', 'userId') ?? ''; + } catch (e) { + console.error('Failed to access localStorage:', e); + return ''; + } +})(); + +export { userEmail, userId }; + +const LeaveOrganization = (): JSX.Element => { + const navigate = useNavigate(); + const { orgId: organizationId } = useParams(); + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [showModal, setShowModal] = useState(false); + const [verificationStep, setVerificationStep] = useState(false); + + /** + * Query to fetch the organization data. + */ + const { + data: orgData, + loading: orgLoading, + error: orgError, + } = useQuery(ORGANIZATIONS_LIST, { + variables: { id: organizationId }, + }); + + /** + * Mutation to remove the member from the organization. + */ + const [removeMember] = useMutation(REMOVE_MEMBER_MUTATION, { + refetchQueries: [ + { + query: USER_ORGANIZATION_CONNECTION, + variables: { id: organizationId }, + }, + ], + onCompleted: () => { + // Use a toast notification or in-app message + setShowModal(false); + toast.success('You have successfully left the organization!'); + navigate(`/user/organizations`); + }, + onError: (err) => { + const isNetworkError = err.networkError !== null; + setError( + isNetworkError + ? 'Unable to process your request. Please check your connection.' + : 'Failed to leave organization. Please try again.', + ); + setLoading(false); + }, + }); + + /** + * Handles the process of leaving the organization. + */ + const handleLeaveOrganization = (): void => { + if (!organizationId || !userId) { + setError('Unable to process request: Missing required information.'); + setLoading(false); + return; + } + setError(''); + setLoading(true); + removeMember({ + variables: { orgid: organizationId, userid: userId }, + }); + }; + + /** + * Verifies the user's email before proceeding. + */ + const handleVerifyAndLeave = (): void => { + if (email.trim().toLowerCase() === userEmail.toLowerCase()) { + handleLeaveOrganization(); + } else { + setError('Verification failed: Email does not match.'); + } + }; + + /** + * Handles the 'Enter' key press. + */ + const handleKeyPress = ( + event: React.KeyboardEvent, + ): void => { + if (event.key === 'Enter') { + event.preventDefault(); + if (verificationStep) { + handleVerifyAndLeave(); + } else { + setVerificationStep(true); + } + } + }; + + if (orgLoading) { + return ( +
+ +

Loading organization details...

+
+ ); + } + if (orgError) + return Error: {orgError.message}; + + if (!orgData?.organizations?.length) { + return

Organization not found

; + } + + const organization = orgData?.organizations[0]; + + return ( +
+
+

{organization?.name}

+

{organization?.description}

+ + + + { + setShowModal(false); + setVerificationStep(false); + setEmail(''); + setError(''); + }} + > + + + Leave Joined Organization + + + + {!verificationStep ? ( + <> +

Are you sure you want to leave this organization?

+

+ This action cannot be undone, and you may need to request access + again if you reconsider. +

+ + ) : ( +
+ + + Enter your email to confirm: + + setEmail(e.target.value)} + onKeyDown={handleKeyPress} + aria-label="confirm-email-input" + /> + + {error && ( + + {error} + + )} +
+ )} +
+ + {!verificationStep ? ( + <> + + + + ) : ( + <> + + + + )} + +
+
+ ); +}; + +export default LeaveOrganization; diff --git a/src/screens/UserPortal/Posts/Posts.test.tsx b/src/screens/UserPortal/Posts/Posts.test.tsx index aa5f03fdcf..433e36f94a 100644 --- a/src/screens/UserPortal/Posts/Posts.test.tsx +++ b/src/screens/UserPortal/Posts/Posts.test.tsx @@ -1,7 +1,7 @@ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import type { RenderResult } from '@testing-library/react'; -import { render, screen, waitFor, within } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import { I18nextProvider } from 'react-i18next'; import userEvent from '@testing-library/user-event'; import { @@ -395,5 +395,8 @@ describe('HomeScreen with invalid orgId', () => { ); const homeEl = await screen.findByTestId('homeEl'); expect(homeEl).toBeInTheDocument(); + + const postCardContainers = screen.queryAllByTestId('postCardContainer'); + expect(postCardContainers).toHaveLength(0); }); }); diff --git a/src/screens/UserPortal/Settings/Settings.test.tsx b/src/screens/UserPortal/Settings/Settings.spec.tsx similarity index 92% rename from src/screens/UserPortal/Settings/Settings.test.tsx rename to src/screens/UserPortal/Settings/Settings.spec.tsx index fd9e1ed350..39d853862f 100644 --- a/src/screens/UserPortal/Settings/Settings.test.tsx +++ b/src/screens/UserPortal/Settings/Settings.spec.tsx @@ -1,5 +1,6 @@ -import React, { act } from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, beforeAll, vi } from 'vitest'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; import { I18nextProvider } from 'react-i18next'; import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; @@ -119,31 +120,27 @@ const resizeWindow = (width: number): void => { async function wait(ms = 100): Promise { await act(async () => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); + vi.advanceTimersByTime(ms); }); } describe('Testing Settings Screen [User Portal]', () => { - // Mock implementation of matchMedia beforeAll(() => { + vi.useFakeTimers(); Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); }); - test('Screen should be rendered properly', async () => { + it('Screen should be rendered properly', async () => { await act(async () => { render( @@ -163,7 +160,7 @@ describe('Testing Settings Screen [User Portal]', () => { expect(screen.queryAllByText('Settings')).not.toBe([]); }); - test('input works properly', async () => { + it('input works properly', async () => { await act(async () => { render( @@ -220,7 +217,7 @@ describe('Testing Settings Screen [User Portal]', () => { expect(screen.getByTestId('profile-picture')).toBeInTheDocument(); }); - test('resetChangesBtn works properly', async () => { + it('resetChangesBtn works properly', async () => { await act(async () => { render( @@ -253,7 +250,7 @@ describe('Testing Settings Screen [User Portal]', () => { expect(screen.getByLabelText('Birth Date')).toHaveValue('2024-03-01'); }); - test('resetChangesBtn works properly when the details are empty', async () => { + it('resetChangesBtn works properly when the details are empty', async () => { await act(async () => { render( @@ -286,7 +283,7 @@ describe('Testing Settings Screen [User Portal]', () => { expect(screen.getByLabelText('Birth Date')).toHaveValue(''); }); - test('sidebar', async () => { + it('sidebar', async () => { await act(async () => { render( @@ -311,7 +308,7 @@ describe('Testing Settings Screen [User Portal]', () => { act(() => openMenuBtn.click()); }); - test('Testing sidebar when the screen size is less than or equal to 820px', async () => { + it('Testing sidebar when the screen size is less than or equal to 820px', async () => { resizeWindow(800); await act(async () => { render( @@ -328,7 +325,6 @@ describe('Testing Settings Screen [User Portal]', () => { }); await wait(); - screen.debug(); const openMenuBtn = screen.queryByTestId('openMenu'); @@ -348,7 +344,7 @@ describe('Testing Settings Screen [User Portal]', () => { } }); - test('renders events attended card correctly', async () => { + it('renders events attended card correctly', async () => { render( @@ -368,7 +364,7 @@ describe('Testing Settings Screen [User Portal]', () => { expect(screen.getByText('No Events Attended')).toBeInTheDocument(); }); - test('renders events attended card correctly with events', async () => { + it('renders events attended card correctly with events', async () => { const mockEventsAttended = [ { _id: '1', title: 'Event 1' }, { _id: '2', title: 'Event 2' }, diff --git a/src/screens/UserPortal/Settings/Settings.tsx b/src/screens/UserPortal/Settings/Settings.tsx index 6038879b7f..cc222468fc 100644 --- a/src/screens/UserPortal/Settings/Settings.tsx +++ b/src/screens/UserPortal/Settings/Settings.tsx @@ -91,6 +91,7 @@ export default function settings(): JSX.Element { * This function sends a mutation request to update the user details * and reloads the page on success. */ + /*istanbul ignore next*/ const handleUpdateUserDetails = async (): Promise => { try { diff --git a/src/screens/UserPortal/UserScreen/UserScreen.test.tsx b/src/screens/UserPortal/UserScreen/UserScreen.spec.tsx similarity index 73% rename from src/screens/UserPortal/UserScreen/UserScreen.test.tsx rename to src/screens/UserPortal/UserScreen/UserScreen.spec.tsx index 642b231a66..65c5e6a650 100644 --- a/src/screens/UserPortal/UserScreen/UserScreen.test.tsx +++ b/src/screens/UserPortal/UserScreen/UserScreen.spec.tsx @@ -1,8 +1,19 @@ +/** + * This file contains unit tests for the UserScreen component. + * + * The tests cover: + * - Rendering of the correct title based on the location. + * - Functionality of the LeftDrawer component. + * - Behavior when the orgId is undefined. + * + * These tests use Vitest for test execution and MockedProvider for mocking GraphQL queries. + */ + import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, vi, beforeEach, expect } from 'vitest'; import { MockedProvider } from '@apollo/react-testing'; -import { fireEvent, render, screen } from '@testing-library/react'; import { I18nextProvider } from 'react-i18next'; -import 'jest-location-mock'; import { Provider } from 'react-redux'; import { BrowserRouter, useNavigate } from 'react-router-dom'; import { store } from 'state/store'; @@ -10,15 +21,20 @@ import i18nForTest from 'utils/i18nForTest'; import UserScreen from './UserScreen'; import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; import { StaticMockLink } from 'utils/StaticMockLink'; +import '@testing-library/jest-dom'; let mockID: string | undefined = '123'; let mockLocation: string | undefined = '/user/organization/123'; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: mockID }), - useLocation: () => ({ pathname: mockLocation }), -})); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: mockID }), + useLocation: () => ({ pathname: mockLocation }), + useNavigate: vi.fn(), // Mock only the necessary parts + }; +}); const MOCKS = [ { @@ -72,8 +88,13 @@ const clickToggleMenuBtn = (toggleButton: HTMLElement): void => { fireEvent.click(toggleButton); }; -describe('Testing LeftDrawer in OrganizationScreen', () => { - test('renders the correct title based on the titleKey for posts', () => { +describe('UserScreen tests with LeftDrawer functionality', () => { + beforeEach(() => { + mockID = '123'; + mockLocation = '/user/organization/123'; + }); + + it('renders the correct title for posts', () => { render( @@ -90,7 +111,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { expect(titleElement).toHaveTextContent('Posts'); }); - test('renders the correct title based on the titleKey', () => { + it('renders the correct title for people', () => { mockLocation = '/user/people/123'; render( @@ -109,7 +130,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { expect(titleElement).toHaveTextContent('People'); }); - test('LeftDrawer should toggle correctly based on window size and user interaction', async () => { + it('toggles LeftDrawer correctly based on window size and user interaction', () => { render( @@ -121,27 +142,30 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { , ); + const toggleButton = screen.getByTestId('closeMenu') as HTMLElement; const icon = toggleButton.querySelector('i'); - // Resize window to a smaller width + // Resize to small screen and check toggle state resizeWindow(800); clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-left'); - // Resize window back to a larger width + // Resize to large screen and check toggle state resizeWindow(1000); clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-right'); + // Check state on re-click clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-left'); }); - test('should be redirected to root when orgId is undefined', async () => { + it('redirects to root when orgId is undefined', () => { mockID = undefined; - const navigate = jest.fn(); - jest.spyOn({ useNavigate }, 'useNavigate').mockReturnValue(navigate); + const navigate = vi.fn(); + vi.spyOn({ useNavigate }, 'useNavigate').mockReturnValue(navigate); + render( @@ -153,6 +177,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { , ); + expect(window.location.pathname).toEqual('/'); }); }); diff --git a/src/screens/UserPortal/UserScreen/UserScreen.tsx b/src/screens/UserPortal/UserScreen/UserScreen.tsx index bcb1d867f3..39b422858f 100644 --- a/src/screens/UserPortal/UserScreen/UserScreen.tsx +++ b/src/screens/UserPortal/UserScreen/UserScreen.tsx @@ -20,6 +20,7 @@ const map: InterfaceMapType = { campaigns: 'userCampaigns', pledges: 'userPledges', volunteer: 'userVolunteer', + leaveorg: 'leaveOrganization', }; /** diff --git a/src/screens/UserPortal/Volunteer/Actions/Actions.test.tsx b/src/screens/UserPortal/Volunteer/Actions/Actions.spec.tsx similarity index 56% rename from src/screens/UserPortal/Volunteer/Actions/Actions.test.tsx rename to src/screens/UserPortal/Volunteer/Actions/Actions.spec.tsx index ce64d98adf..dea26c5fe1 100644 --- a/src/screens/UserPortal/Volunteer/Actions/Actions.test.tsx +++ b/src/screens/UserPortal/Volunteer/Actions/Actions.spec.tsx @@ -1,3 +1,10 @@ +/** + * Unit tests for the Actions component. + * + * This file contains tests for the Actions component to ensure it behaves as expected + * under various scenarios. + */ + import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { LocalizationProvider } from '@mui/x-date-pickers'; @@ -15,6 +22,7 @@ import Actions from './Actions'; import type { ApolloLink } from '@apollo/client'; import { MOCKS, EMPTY_MOCKS, ERROR_MOCKS } from './Actions.mocks'; import useLocalStorage from 'utils/useLocalstorage'; +import { describe, it, beforeAll, beforeEach, afterAll, vi } from 'vitest'; const { setItem } = useLocalStorage(); @@ -39,6 +47,18 @@ const debounceWait = async (ms = 300): Promise => { }); }); }; +const mockNavigate = vi.fn(); + +const expectVitestToBeInTheDocument = (element: HTMLElement): void => { + expect(element).toBeInTheDocument(); +}; + +const expectElementToHaveTextContent = ( + element: HTMLElement, + text: string, +): void => { + expect(element).toHaveTextContent(text); +}; const renderActions = (link: ApolloLink): RenderResult => { return render( @@ -64,10 +84,13 @@ const renderActions = (link: ApolloLink): RenderResult => { describe('Testing Actions Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); // Import the actual implementation + return { + ...actual, + useNavigate: () => mockNavigate, // Replace useNavigate with the mock + }; + }); }); beforeEach(() => { @@ -75,7 +98,7 @@ describe('Testing Actions Screen', () => { }); afterAll(() => { - jest.clearAllMocks(); + vi.restoreAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { @@ -99,95 +122,109 @@ describe('Testing Actions Screen', () => { ); await waitFor(() => { - expect(screen.getByTestId('paramsError')).toBeInTheDocument(); + expectVitestToBeInTheDocument(screen.getByTestId('paramsError')); }); }); it('should render Actions screen', async () => { renderActions(link1); - const searchInput = await screen.findByTestId('searchBy'); - expect(searchInput).toBeInTheDocument(); + await waitFor(async () => { + const searchInput = await screen.findByTestId('searchBy'); + expectVitestToBeInTheDocument(searchInput); - const assigneeName = await screen.findAllByTestId('assigneeName'); - expect(assigneeName[0]).toHaveTextContent('Teresa Bradley'); + const assigneeName = await screen.findAllByTestId('assigneeName'); + expectElementToHaveTextContent(assigneeName[0], 'Teresa Bradley'); + }); }); it('Check Sorting Functionality', async () => { renderActions(link1); + const searchInput = await screen.findByTestId('searchBy'); - expect(searchInput).toBeInTheDocument(); + expectVitestToBeInTheDocument(searchInput); let sortBtn = await screen.findByTestId('sort'); - expect(sortBtn).toBeInTheDocument(); + expectVitestToBeInTheDocument(sortBtn); // Sort by dueDate_DESC fireEvent.click(sortBtn); const dueDateDESC = await screen.findByTestId('dueDate_DESC'); - expect(dueDateDESC).toBeInTheDocument(); + expectVitestToBeInTheDocument(dueDateDESC); fireEvent.click(dueDateDESC); - let assigneeName = await screen.findAllByTestId('assigneeName'); - expect(assigneeName[0]).toHaveTextContent('Group 1'); + await waitFor(() => { + const assigneeName = screen.getAllByTestId('assigneeName'); + expectElementToHaveTextContent(assigneeName[0], 'Group 1'); + }); // Sort by dueDate_ASC sortBtn = await screen.findByTestId('sort'); - expect(sortBtn).toBeInTheDocument(); + expectVitestToBeInTheDocument(sortBtn); fireEvent.click(sortBtn); const dueDateASC = await screen.findByTestId('dueDate_ASC'); - expect(dueDateASC).toBeInTheDocument(); + expectVitestToBeInTheDocument(dueDateASC); fireEvent.click(dueDateASC); - assigneeName = await screen.findAllByTestId('assigneeName'); - expect(assigneeName[0]).toHaveTextContent('Teresa Bradley'); + await waitFor(() => { + const assigneeName = screen.getAllByTestId('assigneeName'); + expectElementToHaveTextContent(assigneeName[0], 'Teresa Bradley'); + }); }); it('Search by Assignee name', async () => { renderActions(link1); - const searchInput = await screen.findByTestId('searchBy'); - expect(searchInput).toBeInTheDocument(); + await waitFor(async () => { + const searchInput = await screen.findByTestId('searchBy'); + expectVitestToBeInTheDocument(searchInput); - const searchToggle = await screen.findByTestId('searchByToggle'); - expect(searchToggle).toBeInTheDocument(); - userEvent.click(searchToggle); + const searchToggle = await screen.findByTestId('searchByToggle'); + expectVitestToBeInTheDocument(searchToggle); + userEvent.click(searchToggle); - const searchByAssignee = await screen.findByTestId('assignee'); - expect(searchByAssignee).toBeInTheDocument(); - userEvent.click(searchByAssignee); + const searchByAssignee = await screen.findByTestId('assignee'); + expectVitestToBeInTheDocument(searchByAssignee); + userEvent.click(searchByAssignee); - userEvent.type(searchInput, '1'); + userEvent.type(searchInput, '1'); + }); await debounceWait(); - const assigneeName = await screen.findAllByTestId('assigneeName'); - expect(assigneeName[0]).toHaveTextContent('Group 1'); + await waitFor(async () => { + const assigneeName = await screen.findAllByTestId('assigneeName'); + expectElementToHaveTextContent(assigneeName[0], 'Group 1'); + }); }); it('Search by Category name', async () => { renderActions(link1); - const searchInput = await screen.findByTestId('searchBy'); - expect(searchInput).toBeInTheDocument(); + await waitFor(async () => { + const searchInput = await screen.findByTestId('searchBy'); + expectVitestToBeInTheDocument(searchInput); - const searchToggle = await screen.findByTestId('searchByToggle'); - expect(searchToggle).toBeInTheDocument(); - userEvent.click(searchToggle); + const searchToggle = await screen.findByTestId('searchByToggle'); + expectVitestToBeInTheDocument(searchToggle); + userEvent.click(searchToggle); - const searchByCategory = await screen.findByTestId('category'); - expect(searchByCategory).toBeInTheDocument(); - userEvent.click(searchByCategory); + const searchByCategory = await screen.findByTestId('category'); + expectVitestToBeInTheDocument(searchByCategory); + userEvent.click(searchByCategory); - // Search by name on press of ENTER - userEvent.type(searchInput, '1'); + userEvent.type(searchInput, '1'); + }); await debounceWait(); - const assigneeName = await screen.findAllByTestId('assigneeName'); - expect(assigneeName[0]).toHaveTextContent('Teresa Bradley'); + await waitFor(() => { + const assigneeName = screen.getAllByTestId('assigneeName'); + expectElementToHaveTextContent(assigneeName[0], 'Teresa Bradley'); + }); }); it('should render screen with No Actions', async () => { renderActions(link3); await waitFor(() => { - expect(screen.getByTestId('searchBy')).toBeInTheDocument(); - expect(screen.getByText(t.noActionItems)).toBeInTheDocument(); + expectVitestToBeInTheDocument(screen.getByTestId('searchBy')); + expectVitestToBeInTheDocument(screen.getByText(t.noActionItems)); }); }); @@ -195,7 +232,7 @@ describe('Testing Actions Screen', () => { renderActions(link2); await waitFor(() => { - expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); + expectVitestToBeInTheDocument(screen.getByTestId('errorMsg')); }); }); @@ -205,7 +242,10 @@ describe('Testing Actions Screen', () => { const checkbox = await screen.findAllByTestId('statusCheckbox'); userEvent.click(checkbox[0]); - expect(await screen.findByText(t.actionItemStatus)).toBeInTheDocument(); + await waitFor(async () => { + const element = await screen.findByText(t.actionItemStatus); // Resolve the promise + expectVitestToBeInTheDocument(element); // Now assert the resolved element + }); userEvent.click(await screen.findByTestId('modalCloseBtn')); }); @@ -215,7 +255,10 @@ describe('Testing Actions Screen', () => { const viewItemBtn = await screen.findAllByTestId('viewItemBtn'); userEvent.click(viewItemBtn[0]); - expect(await screen.findByText(t.actionItemDetails)).toBeInTheDocument(); + await waitFor(() => { + expectVitestToBeInTheDocument(screen.getByText(t.actionItemDetails)); + }); + userEvent.click(await screen.findByTestId('modalCloseBtn')); }); }); diff --git a/src/screens/UserPortal/Volunteer/Groups/GroupModal.tsx b/src/screens/UserPortal/Volunteer/Groups/GroupModal.tsx index 4ae162cd70..27f461b462 100644 --- a/src/screens/UserPortal/Volunteer/Groups/GroupModal.tsx +++ b/src/screens/UserPortal/Volunteer/Groups/GroupModal.tsx @@ -5,7 +5,7 @@ import type { InterfaceVolunteerGroupInfo, InterfaceVolunteerMembership, } from 'utils/interfaces'; -import styles from 'screens/EventVolunteers/EventVolunteers.module.css'; +import styles from 'style/app.module.css'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from '@apollo/client'; diff --git a/src/screens/UserPortal/Volunteer/Groups/Groups.tsx b/src/screens/UserPortal/Volunteer/Groups/Groups.tsx index 3941f461d5..160dc0b23a 100644 --- a/src/screens/UserPortal/Volunteer/Groups/Groups.tsx +++ b/src/screens/UserPortal/Volunteer/Groups/Groups.tsx @@ -16,7 +16,7 @@ import { } from '@mui/x-data-grid'; import { debounce, Stack } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; -import styles from 'screens/EventVolunteers/EventVolunteers.module.css'; +import styles from '../../../../style/app.module.css'; import { EVENT_VOLUNTEER_GROUP_LIST } from 'GraphQl/Queries/EventVolunteerQueries'; import VolunteerGroupViewModal from 'screens/EventVolunteers/VolunteerGroups/VolunteerGroupViewModal'; import useLocalStorage from 'utils/useLocalstorage'; diff --git a/src/screens/UserPortal/Volunteer/Invitations/Invitations.test.tsx b/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx similarity index 96% rename from src/screens/UserPortal/Volunteer/Invitations/Invitations.test.tsx rename to src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx index 2c0cafc6a9..867f95c1aa 100644 --- a/src/screens/UserPortal/Volunteer/Invitations/Invitations.test.tsx +++ b/src/screens/UserPortal/Volunteer/Invitations/Invitations.spec.tsx @@ -21,14 +21,24 @@ import { } from './Invitations.mocks'; import { toast } from 'react-toastify'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi, expect } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: 'orgId' }), + useNavigate: vi.fn(), + }; +}); + const { setItem } = useLocalStorage(); const link1 = new StaticMockLink(MOCKS); @@ -79,19 +89,12 @@ const renderInvitations = (link: ApolloLink): RenderResult => { }; describe('Testing Invvitations Screen', () => { - beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); - }); - beforeEach(() => { setItem('userId', 'userId'); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { diff --git a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx similarity index 90% rename from src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx rename to src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx index 43e0b15cdb..f636d8d8d4 100644 --- a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx +++ b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx @@ -21,11 +21,20 @@ import { } from './UpcomingEvents.mocks'; import { toast } from 'react-toastify'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Unit tests for the UpcomingEvents component. + * + * This file contains tests to verify the functionality and behavior of the UpcomingEvents component + * under various scenarios, including successful data fetching, error handling, and user interactions. + * Mocked dependencies are used to ensure isolated testing of the component. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -81,10 +90,13 @@ const renderUpcomingEvents = (link: ApolloLink): RenderResult => { describe('Testing Upcoming Events Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: 'orgId' }), + }; + }); }); beforeEach(() => { @@ -92,7 +104,7 @@ describe('Testing Upcoming Events Screen', () => { }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { diff --git a/src/screens/Users/Users.module.css b/src/screens/Users/Users.module.css deleted file mode 100644 index 0750dba108..0000000000 --- a/src/screens/Users/Users.module.css +++ /dev/null @@ -1,95 +0,0 @@ -.btnsContainer { - display: flex; - margin: 2.5rem 0 2.5rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; -} - -.btnsContainer .btnsBlock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainer .inputContainer { - flex: 1; - position: relative; -} -.btnsContainer .input { - width: 70%; - position: relative; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .inputContainer button { - width: 52px; -} - -.listBox { - width: 100%; - flex: 1; -} - -.notFound { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - .btnsContainer .input { - width: 100%; - } - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock button { - margin: 0; - } - - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; - } -} - -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 72acba5b5c..b046679a00 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -16,7 +16,7 @@ import TableLoader from 'components/TableLoader/TableLoader'; import UsersTableItem from 'components/UsersTableItem/UsersTableItem'; import InfiniteScroll from 'react-infinite-scroll-component'; import type { InterfaceQueryUserListItem } from 'utils/interfaces'; -import styles from './Users.module.css'; +import styles from '../../style/app.module.css'; import useLocalStorage from 'utils/useLocalstorage'; import type { ApolloError } from '@apollo/client'; /** @@ -70,6 +70,7 @@ const Users = (): JSX.Element => { const { getItem } = useLocalStorage(); const perPageResult = 12; + const tableLoaderRowLength = 4; const [isLoading, setIsLoading] = useState(true); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); @@ -91,8 +92,16 @@ const Users = (): JSX.Element => { }: { data?: { users: InterfaceQueryUserListItem[] }; loading: boolean; - fetchMore: any; - refetch: any; + fetchMore: (options: { + variables: Record; + updateQuery: ( + previousQueryResult: { users: InterfaceQueryUserListItem[] }, + options: { + fetchMoreResult?: { users: InterfaceQueryUserListItem[] }; + }, + ) => { users: InterfaceQueryUserListItem[] }; + }) => void; + refetch: (variables?: Record) => void; error?: ApolloError; } = useQuery(USER_LIST, { variables: { @@ -171,9 +180,11 @@ const Users = (): JSX.Element => { setHasMore(true); }; - const handleSearchByEnter = (e: any): void => { + const handleSearchByEnter = ( + e: React.KeyboardEvent, + ): void => { if (e.key === 'Enter') { - const { value } = e.target; + const { value } = e.target as HTMLInputElement; handleSearch(value); } }; @@ -211,16 +222,16 @@ const Users = (): JSX.Element => { { fetchMoreResult, }: { - fetchMoreResult: { users: InterfaceQueryUserListItem[] } | undefined; + fetchMoreResult?: { users: InterfaceQueryUserListItem[] }; }, - ): { users: InterfaceQueryUserListItem[] } | undefined => { + ) => { setIsLoadingMore(false); - if (!fetchMoreResult) return prev; + if (!fetchMoreResult) return prev || { users: [] }; if (fetchMoreResult.users.length < perPageResult) { setHasMore(false); } return { - users: [...(prev?.users || []), ...(fetchMoreResult.users || [])], + users: [...(prev?.users || []), ...fetchMoreResult.users], }; }, }); @@ -401,80 +412,87 @@ const Users = (): JSX.Element => { usersData && displayedUsers.length === 0 && searchByName.length > 0 ? ( -
+

{tCommon('noResultsFoundFor')} "{searchByName}"

-
+
) : isLoading == false && usersData === undefined && displayedUsers.length === 0 ? ( -
+

{t('noUserFound')}

) : (
- {isLoading ? ( - - ) : ( - - } - hasMore={hasMore} - className={styles.listBox} - data-testid="users-list" - endMessage={ -
-
{tCommon('endOfResults')}
-
- } - > - - - - {headerTitles.map((title: string, index: number) => { + {isLoading && ( + + )} + + } + hasMore={hasMore} + className={styles.listBox} + data-testid="users-list" + endMessage={ +
+
{tCommon('endOfResults')}
+
+ } + > +
+ + + {headerTitles.map((title: string, index: number) => { + return ( + + ); + })} + + + + {usersData && + displayedUsers.map( + (user: InterfaceQueryUserListItem, index: number) => { return ( - + ); - })} - - - - {usersData && - displayedUsers.map( - (user: InterfaceQueryUserListItem, index: number) => { - return ( - - ); - }, - )} - -
+ {title} +
- {title} -
-
- )} + }, + )} + + +
)} ); }; - export default Users; diff --git a/src/setup/askForCustomPort/askForCustomPort.test.ts b/src/setup/askForCustomPort/askForCustomPort.test.ts index 0df6259ba1..c46f7b3f91 100644 --- a/src/setup/askForCustomPort/askForCustomPort.test.ts +++ b/src/setup/askForCustomPort/askForCustomPort.test.ts @@ -1,24 +1,110 @@ import inquirer from 'inquirer'; -import { askForCustomPort } from './askForCustomPort'; +import { askForCustomPort, validatePort } from './askForCustomPort'; jest.mock('inquirer'); describe('askForCustomPort', () => { - test('should return default port if user provides no input', async () => { - jest - .spyOn(inquirer, 'prompt') - .mockResolvedValueOnce({ customPort: '4321' }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('basic port validation', () => { + test('should return default port if user provides no input', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: '4321' }); + + const result = await askForCustomPort(); + expect(result).toBe(4321); + }); + + test('should return user-provided port', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: '8080' }); + + const result = await askForCustomPort(); + expect(result).toBe(8080); + }); + + test('should return validation error if port not between 1 and 65535', () => { + expect(validatePort('abcd')).toBe( + 'Please enter a valid port number between 1 and 65535.', + ); + expect(validatePort('-1')).toBe( + 'Please enter a valid port number between 1 and 65535.', + ); + expect(validatePort('70000')).toBe( + 'Please enter a valid port number between 1 and 65535.', + ); + }); + }); - const result = await askForCustomPort(); - expect(result).toBe('4321'); + describe('retry mechanism', () => { + test('should handle invalid port input and prompt again', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: 'abcd' }) + .mockResolvedValueOnce({ customPort: '8080' }); + + const result = await askForCustomPort(); + expect(result).toBe(8080); + }); + + test('should return default port after maximum retry attempts', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt1' }) + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt2' }) + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt3' }) + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt4' }) + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt5' }) + .mockResolvedValueOnce({ customPort: 'invalid-port-attempt6' }); + + const result = await askForCustomPort(); + expect(result).toBe(4321); + }); }); - test('should return user-provided port', async () => { - jest - .spyOn(inquirer, 'prompt') - .mockResolvedValueOnce({ customPort: '8080' }); + describe('reserved ports', () => { + test('should return user-provided port after confirming reserved port', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: true }); + + const result = await askForCustomPort(); + expect(result).toBe(80); + }); + + test('should re-prompt user for port if reserved port confirmation is denied', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '8080' }); + + const result = await askForCustomPort(); + expect(result).toBe(8080); + }); + + test('should return default port if reserved port confirmation is denied after maximum retry attempts', async () => { + jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '80' }) + .mockResolvedValueOnce({ confirmPort: false }) + .mockResolvedValueOnce({ customPort: '80' }); - const result = await askForCustomPort(); - expect(result).toBe('8080'); + const result = await askForCustomPort(); + expect(result).toBe(4321); + }); }); }); diff --git a/src/setup/askForCustomPort/askForCustomPort.ts b/src/setup/askForCustomPort/askForCustomPort.ts index 8a923f678f..dd0fd51854 100644 --- a/src/setup/askForCustomPort/askForCustomPort.ts +++ b/src/setup/askForCustomPort/askForCustomPort.ts @@ -1,14 +1,63 @@ import inquirer from 'inquirer'; -export async function askForCustomPort(): Promise { - const { customPort } = await inquirer.prompt([ +const DEFAULT_PORT = 4321; +const MAX_RETRY_ATTEMPTS = 5; + +export function validatePort(input: string): string | boolean { + const port = Number(input); + if ( + Number.isNaN(port) || + !Number.isInteger(port) || + port <= 0 || + port > 65535 + ) { + return 'Please enter a valid port number between 1 and 65535.'; + } + return true; +} + +export async function reservedPortWarning(port: number): Promise { + const { confirmPort } = await inquirer.prompt<{ confirmPort: boolean }>([ { - type: 'input', - name: 'customPort', - message: - 'Enter custom port for development server (leave blank for default 4321):', - default: 4321, + type: 'confirm', + name: 'confirmPort', + message: `Port ${port} is a reserved port. Are you sure you want to use it?`, + default: false, }, ]); - return customPort; + + return confirmPort; +} + +export async function askForCustomPort(): Promise { + let remainingAttempts = MAX_RETRY_ATTEMPTS; + + while (remainingAttempts--) { + const { customPort } = await inquirer.prompt<{ customPort: string }>([ + { + type: 'input', + name: 'customPort', + message: `Enter custom port for development server (leave blank for default ${DEFAULT_PORT}):`, + default: DEFAULT_PORT.toString(), + validate: validatePort, + }, + ]); + + if (customPort && validatePort(customPort) === true) { + if (Number(customPort) >= 1024) { + return Number(customPort); + } + + if ( + Number(customPort) < 1024 && + (await reservedPortWarning(Number(customPort))) + ) { + return Number(customPort); + } + } + } + console.log( + `\nMaximum attempts reached. Using default port ${DEFAULT_PORT}.`, + ); + return DEFAULT_PORT; } diff --git a/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.test.ts b/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.test.ts index b1490222b4..3a11a0d799 100644 --- a/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.test.ts +++ b/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.test.ts @@ -30,17 +30,9 @@ describe('askForTalawaApiUrl', () => { }); test('should return the default endpoint when the user does not enter anything', async () => { - const mockPrompt = jest - .spyOn(inquirer, 'prompt') - .mockImplementation(async (questions: any) => { - const answers: Record = {}; - questions.forEach( - (question: { name: string | number; default: any }) => { - answers[question.name] = question.default; - }, - ); - return answers; - }); + const mockPrompt = jest.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + endpoint: 'http://localhost:4000/graphql/', + }); const result = await askForTalawaApiUrl(); diff --git a/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.ts b/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.ts index 97daa1ac89..713ed7dc68 100644 --- a/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.ts +++ b/src/setup/askForTalawaApiUrl/askForTalawaApiUrl.ts @@ -1,7 +1,7 @@ import inquirer from 'inquirer'; export async function askForTalawaApiUrl(): Promise { - const { endpoint } = await inquirer.prompt([ + const { endpoint } = await inquirer.prompt<{ endpoint: string }>([ { type: 'input', name: 'endpoint', diff --git a/src/setupTests.ts b/src/setupTests.ts index eac7093309..d204b3ddc9 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -32,4 +32,4 @@ jestPreviewConfigure({ autoPreview: true, }); -jest.setTimeout(15000); +jest.setTimeout(18000); diff --git a/src/state/reducers/userRoutersReducer.test.ts b/src/state/reducers/userRoutersReducer.test.ts index e2987dcd38..67f075fd44 100644 --- a/src/state/reducers/userRoutersReducer.test.ts +++ b/src/state/reducers/userRoutersReducer.test.ts @@ -18,6 +18,7 @@ describe('Testing Routes reducer', () => { { name: 'Donate', url: 'user/donate/undefined' }, { name: 'Campaigns', url: 'user/campaigns/undefined' }, { name: 'My Pledges', url: 'user/pledges/undefined' }, + { name: 'Leave Organization', url: 'user/leaveorg/undefined' }, ], components: [ { @@ -44,6 +45,11 @@ describe('Testing Routes reducer', () => { component: 'Campaigns', }, { name: 'My Pledges', comp_id: 'pledges', component: 'Pledges' }, + { + name: 'Leave Organization', + comp_id: 'leaveorg', + component: 'LeaveOrganization', + }, ], }); }); @@ -64,6 +70,7 @@ describe('Testing Routes reducer', () => { { name: 'Donate', url: 'user/donate/orgId' }, { name: 'Campaigns', url: 'user/campaigns/orgId' }, { name: 'My Pledges', url: 'user/pledges/orgId' }, + { name: 'Leave Organization', url: 'user/leaveorg/orgId' }, ], components: [ { @@ -90,6 +97,11 @@ describe('Testing Routes reducer', () => { component: 'Campaigns', }, { name: 'My Pledges', comp_id: 'pledges', component: 'Pledges' }, + { + name: 'Leave Organization', + comp_id: 'leaveorg', + component: 'LeaveOrganization', + }, ], }); }); diff --git a/src/state/reducers/userRoutesReducer.ts b/src/state/reducers/userRoutesReducer.ts index e1bf5de0dc..3e99c51b3f 100644 --- a/src/state/reducers/userRoutesReducer.ts +++ b/src/state/reducers/userRoutesReducer.ts @@ -63,6 +63,11 @@ const components: ComponentType[] = [ component: 'Campaigns', }, { name: 'My Pledges', comp_id: 'pledges', component: 'Pledges' }, + { + name: 'Leave Organization', + comp_id: 'leaveorg', + component: 'LeaveOrganization', + }, ]; const generateRoutes = ( diff --git a/src/style/app.module.css b/src/style/app.module.css index def1ac0490..793b403d94 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -1,5 +1,142 @@ -.noOutline input { - outline: none; +:root { + --brown-color: #555555; + --dropdown-hover-color: #eff1f7; + --grey-bg-color: #eaebef; + --subtle-blue-grey: #7c9beb; + --subtle-blue-grey-hover: #5f7e91; + --modal-width: 670px; + --modal-max-width: 680px; + --input-shadow-color: #dddddd; + --delete-button-bg: #f8d6dc; + --delete-button-color: #ff4d4f; + --search-button-bg: #a8c7fa; + --search-button-border: #555555; + --table-image-size: 50px; + --table-head-bg: var( + --bs-primary, + blue + ); /* Assuming var(--bs-primary) is defined elsewhere */ + --table-head-color: white; + --table-header-color: var(--bs-greyish-black, black); + --table-head-radius: 20px; + --table-bg-color: #eaebef; + --tablerow-bg-color: #eff1f7; + --row-background: var(--bs-white, white); + --font-size-header: 16px; +} +.fonts { + color: #707070; +} + +.fonts > span { + font-weight: 600; +} + +.cards { + width: 45%; + background: #fcfcfc; + margin: 10px 20px; + padding: 20px 30px; + border-radius: 5px; + border: 1px solid #e8e8e8; + box-shadow: 0 3px 5px #c9c9c9; + margin-right: 40px; + color: #737373; +} +.cards > h2 { + font-size: 19px; +} +.cards > h3 { + font-size: 17px; +} +.cards > p { + font-size: 14px; + margin-top: -5px; + margin-bottom: 7px; +} + +.sidebar { + z-index: 0; + padding-top: 5px; + margin: 0; + height: 100%; +} +.sidebar:after { + background-color: #f7f7f7; + position: absolute; + width: 2px; + height: 600px; + top: 10px; + left: 94%; + display: block; +} +.sidebarsticky { + padding-left: 45px; + margin-top: 7px; +} +.sidebarsticky > p { + margin-top: -10px; +} + +.logintitle { + color: #707070; + font-weight: 600; + font-size: 20px; + margin-bottom: 30px; + padding-bottom: 5px; + border-bottom: 3px solid #31bb6b; + width: 15%; +} +.searchtitle { + color: #707070; + font-weight: 600; + font-size: 18px; + margin-bottom: 20px; + padding-bottom: 5px; + border-bottom: 3px solid #31bb6b; + width: 60%; +} + +.admindetails { + display: flex; + justify-content: space-between; +} +.admindetails > p { + margin-top: -12px; + margin-right: 30px; +} + +.mainpageright > hr { + margin-top: 20px; + width: 100%; + margin-left: -15px; + margin-right: -15px; + margin-bottom: 20px; +} +.justifysp { + display: flex; + justify-content: space-between; +} +@media screen and (max-width: 575.5px) { + .justifysp { + padding-left: 55px; + display: flex; + justify-content: space-between; + width: 100%; + } +} + +.sidebarsticky > input { + text-decoration: none; + margin-bottom: 50px; + border-color: #e8e5e5; + width: 80%; + border-radius: 7px; + padding-top: 5px; + padding-bottom: 5px; + padding-right: 10px; + padding-left: 10px; + box-shadow: none; } .noOutline:is(:hover, :focus, :active, :focus-visible, .show) { @@ -36,8 +173,8 @@ .dropdown { background-color: white; - border: 1px solid var(--dropdown-border-color); - color: var(--dropdown-text-color); + border: 1px solid var(--brown-color); + color: var(--brown-color); position: relative; display: inline-block; margin-top: 10px; @@ -46,13 +183,13 @@ .dropdown:is(:hover, :focus, :active, :focus-visible, .show) { background-color: transparent !important; - border: 1px solid var(--dropdown-border-color); - color: var(--dropdown-text-color) !important; + border: 1px solid var(--brown-color); + color: var(--brown-color) !important; } .dropdownItem { background-color: white !important; - color: var(--dropdown-text-color) !important; + color: var(--brown-color) !important; border: none !important; } @@ -60,27 +197,28 @@ .dropdownItem:focus, .dropdownItem:active { background-color: var(--dropdown-hover-color) !important; - color: var(--dropdown-text-color) !important; + color: var(--brown-color) !important; outline: none !important; box-shadow: none !important; } .input { - flex: 3; + flex: 1; + position: relative; } .btnsContainer { display: flex; + /* Adjust spacing between items */ margin: 2.5rem 0; align-items: center; gap: 10px; - /* Adjust spacing between items */ - margin: 2.5rem 0; } .btnsContainer .btnsBlock { display: flex; + width: max-content; } .btnsContainer .btnsBlock button { @@ -93,58 +231,18 @@ .btnsContainer .input { flex: 1; position: relative; + max-width: 60%; + justify-content: space-between; } -.btnsContainer .input button { - width: 52px; -} - -.inputField { - margin-top: 10px; - margin-bottom: 10px; - - background-color: white; - box-shadow: 0 1px 1px var(--input-shadow-color); -} - -.btnsContainerBlockAndUnblock { - display: flex; - margin: 2.5rem 0 2.5rem 0; -} - -.btnsContainerBlockAndUnblock .btnsBlockBlockAndUnblock { - display: flex; -} - -.btnsContainerBlockAndUnblock .btnsBlockBlockAndUnblock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainerBlockAndUnblock .inputContainerBlockAndUnblock { - flex: 1; - position: relative; -} - -.btnsContainerBlockAndUnblock .inputBlockAndUnblock { - width: 70%; - position: relative; -} - -.btnsContainerBlockAndUnblock input { +.btnsContainer input { outline: 1px solid var(--bs-gray-400); } -.btnsContainerBlockAndUnblock .inputContainerBlockAndUnblock button { +.btnsContainer .input button { width: 52px; } -.largeBtnsWrapper { - display: flex; -} - .deleteButton { background-color: var(--delete-button-bg); color: var(--delete-button-color); @@ -169,13 +267,13 @@ color: black !important; margin-top: 10px; margin-left: 5px; - border: 1px solid var(--dropdown-border-color); + border: 1px solid var(--brown-color); } .createButton:hover { background-color: var(--grey-bg-color) !important; color: black !important; - border: 1px solid var(--dropdown-border-color) !important; + border: 1px solid var(--brown-color) !important; } .visuallyHidden { @@ -189,6 +287,13 @@ border: 0; } +.inputField { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px var(--input-shadow-color); +} + .inputFieldModal { margin-bottom: 10px; background-color: white; @@ -213,31 +318,6 @@ align-items: center; } -.searchButton:hover { - background-color: var(--search-button-bg); - border-color: var(--search-button-border); -} - -.search { - position: absolute; - z-index: 10; - background-color: var(--search-button-bg); - border-color: var(--search-button-border); - bottom: 0; - right: 0; - height: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -.editButton { - background-color: var(--search-button-bg); - border-color: var(--search-button-border); - color: #555555; - margin-left: 2; -} - .addButton { margin-bottom: 10px; background-color: var(--search-button-bg); @@ -248,17 +328,6 @@ .addButton:hover { background-color: #286fe0; border-color: var(--search-button-border); - /* color: #555555; */ -} - -.modalbtn { - margin-top: 1rem; - display: flex !important; - margin-left: auto; - align-items: center; - background-color: var(--grey-bg-color) !important; - color: black !important; - border: 1px solid var(--dropdown-border-color) !important; } .yesButton { @@ -266,14 +335,14 @@ border-color: var(--search-button-border); } -.mainpageright { - color: var(--dropdown-text-color); +.searchIcon { + color: var(--brown-color); } .infoButton { background-color: var(--search-button-bg); border-color: var(--search-button-border); - color: var(--dropdown-text-color); + color: var(--brown-color); margin-right: 0.5rem; border-radius: 0.25rem; } @@ -299,16 +368,10 @@ margin-top: 20px; } -.mainpageright > hr { - margin-top: 10px; - width: 100%; - margin-left: -15px; - margin-right: -15px; - margin-bottom: 20px; -} - .rowBackground { background-color: var(--row-background); + max-height: 120px; + overflow-y: auto; } .tableHeader { @@ -342,6 +405,11 @@ transform: scale(1.5); color: var(--bs-danger); margin-bottom: 1rem; + /* Add error icon for non-color indication */ + &::before { + content: '⚠️'; + margin-right: 0.5rem; + } } .subTagsLink { @@ -354,19 +422,30 @@ visibility: hidden; } -.subTagsLink:hover { +.subTagsLink:hover, +.subTagsLink:focus { color: var(--subtle-blue-grey-hover); font-weight: 600; text-decoration: underline; } -.subTagsLink:hover i { +.subTagsLink:hover i, +.subTagsLink:focus i { visibility: visible; } -.tagsBreadCrumbs { - color: var(--bs-gray); - cursor: pointer; +.manageTagScrollableDiv { + scrollbar-width: thin; + scrollbar-color: var(--bs-gray-400) var(--bs-white); + + max-height: calc(100vh - 18rem); + overflow: auto; +} + +.tagsBreadCrumbs:hover { + color: var(--bs-blue); + font-weight: 600; + text-decoration: underline; } .orgUserTagsScrollableDiv { @@ -378,6 +457,15 @@ position: sticky; } +/* .checkboxButton{ + background-color: transparent; + } + + .checkboxButton:checked{ + background-color: var(--subtle-blue-grey); + color:white + } */ + input[type='checkbox']:checked + label { background-color: var(--subtle-blue-grey) !important; } @@ -425,7 +513,6 @@ hr { display: flex; justify-content: flex-end; } - .icon { margin: 1px; } @@ -470,113 +557,362 @@ hr { color: var(--bs-primary) !important; } -.custom_table { - border-radius: 20px; - background-color: var(--grey-bg-color); +.pageNotFound { + position: relative; + bottom: 20px; } -.custom_table tbody tr { - background-color: var(--dropdown-hover-color); +.pageNotFound h3 { + font-family: 'Roboto', sans-serif; + font-weight: normal; + letter-spacing: 1px; } -.custom_table tbody tr:hover { - background-color: var(--grey-bg-color); - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); +.pageNotFound .brand span { + margin-top: 50px; + font-size: 40px; +} +.pageNotFound .brand h3 { + font-weight: 300; + margin: 10px 0 0 0; +} +.pageNotFound h1.head { + font-size: 250px; + font-weight: 900; + color: #31bb6b; + letter-spacing: 25px; + margin: 10px 0 0 0; +} +.pageNotFound h1.head span { + position: relative; + display: inline-block; +} +.pageNotFound h1.head span:before, +.pageNotFound h1.head span:after { + position: absolute; + top: 50%; + width: 50%; + height: 1px; + background: #fff; + content: ''; +} +.pageNotFound h1.head span:before { + left: -55%; +} +.pageNotFound h1.head span:after { + right: -55%; } -.custom_table tbody tr:focus-within { - outline: 2px solid #000; - outline-offset: -2px; +.pledgeContainer { + margin: 0.6rem 0; } -.custom_table tbody td:focus { - outline: 2px solid #000; - outline-offset: -2px; +.container { + min-height: 100vh; } -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } +.pledgeModal { + max-width: 80vw; + margin-top: 2vh; + margin-left: 13vw; +} - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } +.greenregbtnPledge { + margin-top: 15px; + border: 1px solid var(--bs-gray-300); + box-shadow: 0 2px 2px #e8e5e5; + padding: 10px 10px; + border-radius: 5px; + background-color: var(--bs-primary); + width: 100%; + font-size: 16px; + color: var(--bs-white); + outline: none; + font-weight: 600; + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; +} - .btnsContainer .btnsBlock div { - flex: 1; - } +.btnsContainerPledge { + display: flex; + gap: 0.8rem; + margin: 2.2rem 0 0.8rem 0; +} - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } +.btnsContainerPledge .inputPledge { + flex: 1; + min-width: 18rem; + position: relative; +} - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } +.btnsContainerPledge input { + outline: 1px solid var(--bs-gray-400); } -.listBox { - width: 100%; - flex: 1; +.btnsContainerPledge .inputPledge button { + width: 52px; } -.listTable { - width: 100%; - box-sizing: border-box; - background: #ffffff; - border: 1px solid #0000001f; - border-radius: 24px; +.inputFieldPledge { + background-color: white; + box-shadow: 0 1px 1px #31bb6b; } -.listBox .customTable { - margin-bottom: 0%; +.dropdownPledge { + background-color: white; + border: 1px solid var(--bs-primary); + position: relative; + display: inline-block; + color: var(--bs-primary); } -.requestsTable thead th { - font-size: 20px; - font-weight: 400; - line-height: 24px; - letter-spacing: 0em; - text-align: left; - color: #000000; - border-bottom: 1px solid #dddddd; - padding: 1.5rem; +.rowBackgroundPledge { + background-color: var(--bs-white); + max-height: 120px; } -.notFound { - flex: 1; +.TableImagePledge { + object-fit: cover; + width: calc(var(--table-image-size) / 2) !important; + height: calc(var(--table-image-size) / 2) !important; + border-radius: 100% !important; +} + +.imageContainerPledge { display: flex; - justify-content: center; align-items: center; - flex-direction: column; + justify-content: center; } -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } +.pledgerContainer { + display: flex; + align-items: center; + justify-content: center; + margin: 0.1rem 0.25rem; + gap: 0.25rem; + padding: 0.25rem 0.45rem; + border-radius: 0.35rem; + background-color: var(--bs-primary-rgb, 49, 187, 107, 0.2); + height: 2.2rem; + margin-top: 0.75rem; +} - .btnsContainer .input { - width: 100%; - } +.noOutlinePledge input { + outline: none; +} - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } +.overviewContainer { + display: flex; + gap: 7rem; + width: 100%; + justify-content: space-between; + margin: 1.5rem 0 0 0; + padding: 1.25rem 2rem; + background-color: rgba(255, 255, 255, 0.591); - .btnsContainer .btnsBlock button { - margin: 0; - } + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; + border-radius: 0.5rem; +} - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; +.titleContainer { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.titleContainer h3 { + font-size: 1.75rem; + font-weight: 750; + color: #5e5e5e; + margin-top: 0.2rem; +} + +.titleContainer span { + font-size: 0.9rem; + margin-left: 0.5rem; + font-weight: lighter; + color: #707070; +} + +.raisedAmount { + display: flex; + justify-content: center; + align-items: center; + font-size: 1.25rem; + font-weight: 750; + color: #5e5e5e; +} + +.progressContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex-grow: 1; +} + +.progress { + margin-top: 0.2rem; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.endpoints { + display: flex; + position: relative; + font-size: 0.85rem; +} + +.start { + position: absolute; + top: 0px; +} + +.end { + position: absolute; + top: 0px; + right: 0px; +} + +.moreContainer { + display: flex; + align-items: center; +} + +.moreContainer:hover { + text-decoration: underline; + cursor: pointer; +} + +.popup { + z-index: 50; + border-radius: 0.5rem; + font-family: sans-serif; + font-weight: 500; + font-size: 0.875rem; + margin-top: 0.5rem; + padding: 0.75rem; + border: 1px solid #e2e8f0; + background-color: white; + color: #1e293b; + box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 0.15); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.popupExtra { + max-height: 15rem; + overflow-y: auto; +} + +.toggleGroupPledge { + width: 50%; + min-width: 27.75rem; + margin: 0.5rem 0rem; +} + +.toggleBtnPledge { + padding: 0rem; + height: 30px; + display: flex; + justify-content: center; + align-items: center; +} + +.toggleBtnPledge:hover { + color: #31bb6b !important; +} + +@media (max-width: 1024px) { + .pageNotFound h1.head { + font-size: 200px; + letter-spacing: 25px; + } +} + +@media (max-width: 768px) { + .pageNotFound h1.head { + font-size: 150px; + letter-spacing: 25px; + } +} + +@media (max-width: 640px) { + .pageNotFound h1.head { + font-size: 150px; + letter-spacing: 0; + } +} + +@media (max-width: 480px) { + .pageNotFound .brand h3 { + font-size: 20px; + } + .pageNotFound h1.head { + font-size: 130px; + letter-spacing: 0; + } + .pageNotFound h1.head span:before, + .pageNotFound h1.head span:after { + width: 40%; + } + .pageNotFound h1.head span:before { + left: -45%; + } + .pageNotFound h1.head span:after { + right: -45%; + } + .pageNotFound p { + font-size: 18px; + } +} + +@media (max-width: 320px) { + .pageNotFound .brand h3 { + font-size: 16px; + } + .pageNotFound h1.head { + font-size: 100px; + letter-spacing: 0; + } + .pageNotFound h1.head span:before, + .pageNotFound h1.head span:after { + width: 25%; + } + .pageNotFound h1.head span:before { + left: -30%; + } + .pageNotFound h1.head span:after { + right: -30%; + } +} + +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; + } + + .btnsContainer .btnsBlock div { + flex: 1; + } + + .btnsContainer .btnsBlock div[title='Sort organizations'] { + margin-right: 0.5rem; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; } } @@ -590,12 +926,31 @@ hr { } } +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; + } + + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; + } + + .btnsContainer .btnsBlock button { + margin: 0; + } + + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; + } +} + @-webkit-keyframes load8 { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); @@ -607,7 +962,6 @@ hr { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); @@ -624,11 +978,6 @@ hr { box-shadow: 0 1px 1px #31bb6b; } -.dropdowns { - border: 1px solid #31bb6b; - color: #31bb6b; -} - .fundName { font-weight: 600; cursor: pointer; @@ -649,34 +998,101 @@ hr { margin-left: 13vw; } -.titlemodal { - color: #707070; - font-weight: 600; - font-size: 32px; - width: 65%; - margin-bottom: 0px; +.btnsContainer .btnsBlock button { + margin-left: 1rem; + display: flex; + justify-content: center; + align-items: center; } -.modalCloseBtn { - width: 40px; - height: 40px; - padding: 1rem; +.errorMessage { + margin-top: 25%; display: flex; justify-content: center; align-items: center; + flex-direction: column; +} + +.tableHeaders { + background-color: var(--bs-primary-text-emphasis); + color: var(--bs-white); + font-size: 1rem; +} + +.subTagsLink { + color: var(--bs-blue); + font-weight: 500; + cursor: pointer; + /* Prevent layout shift */ + &::after { + display: block; + content: attr(data-text); + font-weight: 600; + height: 0; + overflow: hidden; + visibility: hidden; + } +} + +.tagsBreadCrumbs { + color: var(--bs-gray); + cursor: pointer; + /* Prevent layout shift */ + &::after { + display: block; + content: attr(data-text); + font-weight: 600; + height: 0; + overflow: hidden; + visibility: hidden; + } +} + +.tagsBreadCrumbs:hover, +.tagsBreadCrumbs:focus { + color: var(--bs-blue); + font-weight: 600; + text-decoration: underline; +} + +.subTagsScrollableDiv { + scrollbar-width: auto; + scrollbar-color: var(--bs-gray-400) var(--bs-white); + + max-height: calc(100vh - 18rem); + overflow: auto; +} + +#individualRadio, +#requestsRadio, +#groupsRadio, +.toggleBtn:hover { + color: var(--brand-primary) !important; +} + +input[type='radio']:checked + label { + background-color: var(--brand-primary-light) !important; +} + +.dropdownToggle { + margin-bottom: 0; + display: flex; +} + +.dropdownModalToggle { + width: 50%; } .greenregbtn { - margin: 1rem 0 0; - margin-top: 15px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; + margin-top: 1rem; + border: 1px solid var(--bs-gray-300); + box-shadow: 0 2px 2px var(--bs-gray-300); padding: 10px 10px; border-radius: 5px; - background-color: #31bb6b; + background-color: var(--bs-primary); width: 100%; font-size: 16px; - color: white; + color: var(--bs-white); outline: none; font-weight: 600; cursor: pointer; @@ -704,13 +1120,132 @@ hr { box-shadow 0.2s; } +.removeFilterIcon { + cursor: pointer; +} + +.searchForm { + display: inline; +} + +.view { + margin-left: 2%; + font-weight: 600; + font-size: 16px; + color: var(--bs-gray-600); +} + +/* header (search, filter, dropdown) */ +.btncon .btnsContainer { + display: flex; + margin: 0.5rem 0 1.5rem 0; +} + +.btncon .btnsContainer .input { + flex: 1; + min-width: 18rem; + position: relative; +} + +.btncon .btnsContainer input { + outline: 1px solid var(--bs-gray-400); +} + +.btncon .btnsContainer .input button { + width: 52px; +} + +.noOutline input:disabled { + -webkit-text-fill-color: black !important; +} + +.noOutline textarea:disabled { + color: var(--bs-black) !important; + opacity: 1; +} + +.inputFields { + box-shadow: 0 1px 1px var(--brand-primary); +} + +.dropdowns { + background-color: white; + border: 1px solid #31bb6b; + position: relative; + display: inline-block; + color: #31bb6b; +} + +/* Action Items Data Grid */ .rowBackgrounds { background-color: var(--bs-white); max-height: 120px; } -.tableHeader { - background-color: var(--bs-primary); - color: var(--bs-white); - font-size: 1rem; +.chipIcon { + height: 0.9rem !important; +} + +.chip { + height: 1.5rem !important; +} + +.active { + background-color: var(--status-active-bg); +} + +.pending { + background-color: var(--status-pending-bg); + color: var(--status-pending-text); + border-color: var(--status-pending-border); +} + +/* Modals */ +.itemModal { + max-width: 80vw; + margin-top: 2vh; + margin-left: 13vw; +} + +.titlemodal { + color: #707070; + font-weight: 600; + font-size: 32px; + width: 65%; + margin-bottom: 0px; +} + +.modalCloseBtn { + width: 40px; + height: 40px; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +.imageContainer { + display: flex; + align-items: center; + justify-content: center; + margin-right: 0.5rem; +} + +.TableImages { + object-fit: cover; + width: var(--image-width, 100%); + height: var(--image-height, auto); + border-radius: 0; + margin-right: var(--image-spacing, 8px); +} + +.avatarContainer { + width: 28px; + height: 26px; +} + +/* Modal Table (Groups & Assignments) */ +.modalTable { + max-height: 220px; + overflow-y: auto; } diff --git a/src/utils/errorHandler.test.tsx b/src/utils/errorHandler.test.tsx index 45f46e6389..f229e8d5fa 100644 --- a/src/utils/errorHandler.test.tsx +++ b/src/utils/errorHandler.test.tsx @@ -11,23 +11,87 @@ jest.mock('react-toastify', () => ({ describe('Test if errorHandler is working properly', () => { const t: TFunction = (key: string) => key; - const tErrors: TFunction = (key: string, options?: Record) => - key; + const tErrors: TFunction = ( + key: string, + options?: Record, + ) => { + if (options) { + console.log(`options are passed, but the function returns only ${key}`); + } + return key; + }; - it('should call toast.error with the correct message if error message is "Failed to fetch"', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call toast.error with the correct message if error message is "Failed to fetch"', async () => { const error = new Error('Failed to fetch'); errorHandler(t, error); expect(toast.error).toHaveBeenCalledWith(tErrors('talawaApiUnavailable')); }); - it('should call toast.error with the error message if it is not "Failed to fetch"', () => { - const error = new Error('Some other error message'); + it('should call toast.error with the correct message if error message contains this substring "Value is not a valid phone number"', () => { + const error = new Error('This value is not a valid phone number'); errorHandler(t, error); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidPhoneNumber')); + }); + + test.each([ + ['EducationGrade', 'invalidEducationGrade'], + ['EmploymentStatus', 'invalidEmploymentStatus'], + ['MaritalStatus', 'invalidMaritalStatus'], + ])('should handle invalid %s error', (field, expectedKey) => { + const error = new Error(`This value does not exist in "${field}"`); + errorHandler(t, error); + expect(toast.error).toHaveBeenCalledWith(tErrors(expectedKey)); + }); + + it('should call toast.error with the correct message if error message contains this substring "status code 400"', () => { + const error = new Error('Server responded with status code 400'); + errorHandler(t, error); + + expect(toast.error).toHaveBeenCalledWith(tErrors('error400')); + }); + + it('should handle error messages with different cases', () => { + errorHandler(t, new Error('VALUE IS NOT A VALID PHONE NUMBER')); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidPhoneNumber')); + + errorHandler(t, new Error('This Value Does Not Exist in "EducationGrade"')); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidEducationGrade')); + }); + it('should call toast.error with the error message if it is an instance of error but have not matched any error message patterns', () => { + const error = new Error('Bandhan sent an error message'); + errorHandler(t, error); expect(toast.error).toHaveBeenCalledWith(error.message); }); + it('should handle different types for the first parameter while still showing error messages', () => { + errorHandler(undefined, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + + errorHandler(null, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + + errorHandler({}, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + }); + + it('should handle non-null but non-Error objects for the error parameter', () => { + errorHandler(t, { message: 'Error message in object' }); + expect(toast.error).toHaveBeenCalledWith( + tErrors('unknownError', { msg: { message: 'Error message in object' } }), + ); + + errorHandler(t, 'Direct error message'); + expect(toast.error).toHaveBeenCalledWith( + tErrors('unknownError', { msg: 'Direct error message' }), + ); + }); + it('should call toast.error with the error message if error object is falsy', () => { const error = null; errorHandler(t, error); diff --git a/src/utils/errorHandler.tsx b/src/utils/errorHandler.tsx index b7a22210a8..e4e543e940 100644 --- a/src/utils/errorHandler.tsx +++ b/src/utils/errorHandler.tsx @@ -5,18 +5,26 @@ import i18n from './i18n'; /** This function is used to handle api errors in the application. It takes in the error object and displays the error message to the user. - If the error is due to the Talawa API being unavailable, it displays a custom message. + If the error is due to the Talawa API being unavailable, it displays a custom message. And for other error cases, it is using regular expression (case-insensitive) to match and show valid messages */ export const errorHandler = (a: unknown, error: unknown): void => { const tErrors: TFunction = i18n.getFixedT(null, 'errors'); if (error instanceof Error) { - switch (error.message) { - case 'Failed to fetch': - toast.error(tErrors('talawaApiUnavailable') as string); - break; - // Add more cases as needed - default: - toast.error(error.message); + const errorMessage = error.message; + if (errorMessage === 'Failed to fetch') { + toast.error(tErrors('talawaApiUnavailable')); + } else if (errorMessage.match(/value is not a valid phone number/i)) { + toast.error(tErrors('invalidPhoneNumber')); + } else if (errorMessage.match(/does not exist in "EducationGrade"/i)) { + toast.error(tErrors('invalidEducationGrade')); + } else if (errorMessage.match(/does not exist in "EmploymentStatus"/i)) { + toast.error(tErrors('invalidEmploymentStatus')); + } else if (errorMessage.match(/does not exist in "MaritalStatus"/i)) { + toast.error(tErrors('invalidMaritalStatus')); + } else if (errorMessage.match(/status code 400/i)) { + toast.error(tErrors('error400')); + } else { + toast.error(errorMessage); } } else { toast.error(tErrors('unknownError', { msg: error }) as string); diff --git a/src/utils/getRefreshToken.ts b/src/utils/getRefreshToken.ts index 5d6f8aa2ce..90b1b55ef1 100644 --- a/src/utils/getRefreshToken.ts +++ b/src/utils/getRefreshToken.ts @@ -14,7 +14,7 @@ export async function refreshToken(): Promise { const { getItem, setItem } = useLocalStorage(); const refreshToken = getItem('refreshToken'); - /* istanbul ignore next */ + try { const { data } = await client.mutate({ mutation: REFRESH_TOKEN_MUTATION, diff --git a/vitest.config.ts b/vitest.config.ts index cd08488b3c..3d071e7534 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; import tsconfigPaths from 'vite-tsconfig-paths'; +import svgrPlugin from 'vite-plugin-svgr'; export default defineConfig({ plugins: [ @@ -10,11 +11,13 @@ export default defineConfig({ include: ['events'], }), tsconfigPaths(), + svgrPlugin(), ], test: { include: ['src/**/*.spec.{js,jsx,ts,tsx}'], globals: true, environment: 'jsdom', + setupFiles: 'vitest.setup.ts', coverage: { enabled: true, provider: 'istanbul', @@ -28,6 +31,7 @@ export default defineConfig({ '**/*.d.ts', 'src/test/**', 'vitest.config.ts', + 'vitest.setup.ts', // Exclude from coverage if necessary ], reporter: ['text', 'html', 'text-summary', 'lcov'], }, diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom';