diff --git a/.env.sample b/.env.sample index fc7fd58..b5d1c44 100644 --- a/.env.sample +++ b/.env.sample @@ -2,3 +2,4 @@ VITE_REACT_RDS_BACKEND_BASE_URL='https://staging-api.realdevsquad.com' VITE_REACT_STATUS_SITE_URL='https://staging-status.realdevsquad.com' VITE_REACT_MEMBERS_SITE_URL='https://staging-members.realdevsquad.com' VITE_REACT_WELCOME_SITE_URL='https://welcome.realdevsquad.com' +VITE_REACT_RDS_BACKEND_BASE_URL='https://localhost:4000' diff --git a/eslint.config.js b/eslint.config.js index a5f2516..c8f8935 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -21,6 +21,7 @@ export default tseslint.config( globals: { ...globals.browser, ...globals.jest, + require: 'readonly', }, parserOptions: { ecmaFeatures: { diff --git a/package.json b/package.json index c89d1e8..4be315d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@eslint/js": "^9.14.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.14", "@types/node": "^22.9.3", "@types/react": "^18.3.12", @@ -52,6 +53,9 @@ "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", "jsdom": "^25.0.1", + "local-ssl-proxy": "^2.0.5", + "msw": "^2.6.6", + "npm-run-all": "^4.1.5", "postcss": "^8.4.47", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76bbfc6..371296c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ devDependencies: '@testing-library/react': specifier: ^16.0.1 version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1)(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -103,6 +106,9 @@ devDependencies: jsdom: specifier: ^25.0.1 version: 25.0.1 + msw: + specifier: ^2.6.6 + version: 2.7.0(@types/node@22.10.1)(typescript@5.6.3) postcss: specifier: ^8.4.47 version: 8.4.49 @@ -126,7 +132,7 @@ devDependencies: version: 5.4.11(@types/node@22.10.1) vitest: specifier: ^2.1.4 - version: 2.1.8(@types/node@22.10.1)(jsdom@25.0.1) + version: 2.1.8(@types/node@22.10.1)(jsdom@25.0.1)(msw@2.7.0) packages: @@ -329,6 +335,25 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@bundled-es-modules/cookie@2.0.1: + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + dependencies: + cookie: 0.7.2 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: true + + /@bundled-es-modules/tough-cookie@0.1.6: + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + dev: true + /@esbuild/aix-ppc64@0.21.5: resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -600,6 +625,48 @@ packages: deprecated: Use @eslint/object-schema instead dev: true + /@inquirer/confirm@5.1.1(@types/node@22.10.1): + resolution: {integrity: sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + dependencies: + '@inquirer/core': 10.1.2(@types/node@22.10.1) + '@inquirer/type': 3.0.2(@types/node@22.10.1) + '@types/node': 22.10.1 + dev: true + + /@inquirer/core@10.1.2(@types/node@22.10.1): + resolution: {integrity: sha512-bHd96F3ezHg1mf/J0Rb4CV8ndCN0v28kUlrHqP7+ECm1C/A+paB7Xh2lbMk6x+kweQC+rZOxM/YeKikzxco8bQ==} + engines: {node: '>=18'} + dependencies: + '@inquirer/figures': 1.0.9 + '@inquirer/type': 3.0.2(@types/node@22.10.1) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + dev: true + + /@inquirer/figures@1.0.9: + resolution: {integrity: sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==} + engines: {node: '>=18'} + dev: true + + /@inquirer/type@3.0.2(@types/node@22.10.1): + resolution: {integrity: sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + dependencies: + '@types/node': 22.10.1 + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -673,6 +740,18 @@ packages: '@jridgewell/sourcemap-codec': 1.5.0 dev: true + /@mswjs/interceptors@0.37.5: + resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -694,6 +773,21 @@ packages: fastq: 1.17.1 dev: true + /@open-draft/deferred-promise@2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + dev: true + + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + dev: true + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -913,6 +1007,15 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: true + /@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0): + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 10.4.0 + dev: true + /@types/aria-query@5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true @@ -946,6 +1049,10 @@ packages: '@babel/types': 7.26.3 dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/estree@1.0.6: resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} dev: true @@ -1004,6 +1111,14 @@ packages: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} dev: true @@ -1186,7 +1301,7 @@ packages: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.8(@types/node@22.10.1)(jsdom@25.0.1) + vitest: 2.1.8(@types/node@22.10.1)(jsdom@25.0.1)(msw@2.7.0) transitivePeerDependencies: - supports-color dev: true @@ -1200,7 +1315,7 @@ packages: tinyrainbow: 1.2.0 dev: true - /@vitest/mocker@2.1.8(vite@5.4.11): + /@vitest/mocker@2.1.8(msw@2.7.0)(vite@5.4.11): resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} peerDependencies: msw: ^2.4.9 @@ -1214,6 +1329,7 @@ packages: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.14 + msw: 2.7.0(@types/node@22.10.1)(typescript@5.6.3) vite: 5.4.11(@types/node@22.10.1) dev: true @@ -1289,6 +1405,13 @@ packages: engines: {node: '>=6'} dev: false + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1614,6 +1737,20 @@ packages: engines: {node: '>=8'} dev: true + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1663,6 +1800,11 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + dev: true + /cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -2540,6 +2682,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2639,6 +2786,11 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: true + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2679,6 +2831,10 @@ packages: dependencies: function-bind: 1.1.2 + /headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + dev: true + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false @@ -2871,6 +3027,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + dev: true + /is-number-object@1.1.0: resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} engines: {node: '>= 0.4'} @@ -3338,6 +3498,45 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /msw@2.7.0(@types/node@22.10.1)(typescript@5.6.3): + resolution: {integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.1.1(@types/node@22.10.1) + '@mswjs/interceptors': 0.37.5 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + graphql: 16.10.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + type-fest: 4.31.0 + typescript: 5.6.3 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + dev: true + + /mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + dev: true + /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -3485,6 +3684,10 @@ packages: word-wrap: 1.2.5 dev: true + /outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3555,6 +3758,10 @@ packages: minipass: 7.1.2 dev: true + /path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + dev: true + /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -3777,11 +3984,21 @@ packages: react-is: 16.13.1 dev: true + /psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + dependencies: + punycode: 2.3.1 + dev: true + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} dev: true + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -3917,9 +4134,13 @@ packages: es-errors: 1.3.0 set-function-name: 2.0.2 + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: false /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -4153,10 +4374,19 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + /std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} dev: true + /strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4415,6 +4645,16 @@ packages: is-number: 7.0.0 dev: true + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} @@ -4466,6 +4706,16 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@4.31.0: + resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==} + engines: {node: '>=16'} + dev: true + /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -4544,6 +4794,11 @@ packages: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /update-browserslist-db@1.1.1(browserslist@4.24.2): resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -4561,6 +4816,13 @@ packages: punycode: 2.3.1 dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -4633,7 +4895,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@2.1.8(@types/node@22.10.1)(jsdom@25.0.1): + /vitest@2.1.8(@types/node@22.10.1)(jsdom@25.0.1)(msw@2.7.0): resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -4660,7 +4922,7 @@ packages: dependencies: '@types/node': 22.10.1 '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11) + '@vitest/mocker': 2.1.8(msw@2.7.0)(vite@5.4.11) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -4800,6 +5062,15 @@ packages: engines: {node: '>=0.10.0'} dev: true + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4844,6 +5115,11 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true @@ -4854,7 +5130,30 @@ packages: hasBin: true dev: true + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + dev: true diff --git a/src/components/FeatureFlagCard.tsx b/src/components/FeatureFlagCard.tsx new file mode 100644 index 0000000..13de098 --- /dev/null +++ b/src/components/FeatureFlagCard.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { FaRegCalendarAlt } from 'react-icons/fa'; +import { MdUpdate } from 'react-icons/md'; +import { formatTimestamp } from '../utils/format-timestamp'; +import { FeatureFlag } from '../services/api'; + +interface FeatureFlagCardProps { + flag: FeatureFlag; + onCardClick: (id: string) => void; +} + +const FeatureFlagCard: React.FC = ({ + flag, + onCardClick, +}) => { + return ( +
onCardClick(flag.id)} + data-testid={`feature-flag-${flag.id}`} + > +
+
+

+ {flag.name} +

+
+ + Status: + + + {flag.status} + +
+
+

{flag.description}

+
+
+
+ + Created: {formatTimestamp(flag.createdAt)} +
+
+ + Updated: {formatTimestamp(flag.updatedAt)} +
+
+
+
+
+ ); +}; + +export default FeatureFlagCard; diff --git a/src/components/Navbar.tsx b/src/components/navbar.tsx similarity index 100% rename from src/components/Navbar.tsx rename to src/components/navbar.tsx diff --git a/src/config.ts b/src/config.ts index 9eb569e..b13c538 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,8 +11,9 @@ const config: Config = { welcomeSiteUrl: import.meta.env.VITE_REACT_WELCOME_SITE_URL, membersSiteUrl: import.meta.env.VITE_REACT_MEMBERS_SITE_URL, statusSiteUrl: import.meta.env.VITE_REACT_STATUS_SITE_URL, - featureFlagUrl: import.meta.env.VITE_REACT_FEATUREFLAG_SITE_URL, rdsBackendBaseUrl: import.meta.env.VITE_REACT_RDS_BACKEND_BASE_URL, + featureFlagBaseUrl: import.meta.env.VITE_REACT_FEATURE_FLAG_BASE_URL, + featureFlagUrl: import.meta.env.VITE_REACT_FEATUREFLAG_SITE_URL, }; export const getConfig = () => { @@ -25,6 +26,8 @@ export const validateEnv = () => { 'welcomeSiteUrl', 'membersSiteUrl', 'statusSiteUrl', + 'rdsBackendBaseUrl', + 'featureFlagBaseUrl', ]; for (const key of requiredVars) { if (!config[key]) { diff --git a/src/pages/FeatureFlagList.tsx b/src/pages/FeatureFlagList.tsx index f560dfa..0896b1f 100644 --- a/src/pages/FeatureFlagList.tsx +++ b/src/pages/FeatureFlagList.tsx @@ -1,8 +1,65 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import FeatureFlagCard from '../components/FeatureFlagCard'; +import Spinner from '../components/Spinner'; +import { getAllFeatureFlags, FeatureFlag } from '../services/api'; + const FeatureFlagList: React.FC = () => { + const [featureFlags, setFeatureFlags] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + const fetchFeatureFlags = async () => { + try { + setIsLoading(true); + setError(null); + const data = await getAllFeatureFlags(); + setFeatureFlags(data); + } catch (err) { + setError( + err instanceof Error ? err.message : 'Failed to fetch feature flags', + ); + } finally { + setIsLoading(false); + } + }; + + fetchFeatureFlags(); + }, []); + + const handleCardClick = (id: string) => { + navigate(`/featureFlag/${id}`); + }; + return ( -
-

Feature Flags

+
+
+

Feature Flags

+
+ + {error && ( +
+ {error} +
+ )} + + {isLoading ? ( +
+ +
+ ) : ( +
+ {featureFlags.map((flag) => ( + + ))} +
+ )}
); }; diff --git a/src/services/api.ts b/src/services/api.ts index e47c0b4..3a9803f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,3 +1,4 @@ +import { FeatureFlag } from '../types/featureFlag'; import { getConfig } from '../config'; const { rdsBackendBaseUrl } = getConfig(); @@ -26,3 +27,21 @@ export const signOut = async () => { } return response.json(); }; + +export const getAllFeatureFlags = async (): Promise => { + try { + const baseUrl = getConfig().featureFlagBaseUrl; + const response = await fetchData(`${baseUrl}/feature-flags`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to fetch feature flags'); + } + + const data = await response.json(); + return data; + } catch (err) { + console.error(err); + throw new Error('Failed to fetch feature flags'); + } +}; diff --git a/src/types/featureFlag.ts b/src/types/featureFlag.ts new file mode 100644 index 0000000..b1466b0 --- /dev/null +++ b/src/types/featureFlag.ts @@ -0,0 +1,10 @@ +export interface FeatureFlag { + id: string; + name: string; + description: string; + status: 'ENABLED' | 'DISABLED'; + createdAt: number; + createdBy: string; + updatedAt: number; + updatedBy: string; +} diff --git a/src/utils/format-timestamp.ts b/src/utils/format-timestamp.ts new file mode 100644 index 0000000..dda0379 --- /dev/null +++ b/src/utils/format-timestamp.ts @@ -0,0 +1,11 @@ +export const formatTimestamp = (timestamp: number): string => { + const date = new Date(timestamp * 1000); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true, + }); +}; diff --git a/tests/components/FeatureFlagCard.test.tsx b/tests/components/FeatureFlagCard.test.tsx new file mode 100644 index 0000000..18258fd --- /dev/null +++ b/tests/components/FeatureFlagCard.test.tsx @@ -0,0 +1,38 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import FeatureFlagCard from '../../src/components/FeatureFlagCard'; +import { mockFeatureFlags } from '../mocks/mockData'; +import { FeatureFlag } from '../../src/services/api'; +import { describe, it, expect, vi } from 'vitest'; + +describe('FeatureFlagCard', () => { + const mockOnCardClick = vi.fn(); + const mockFlag: FeatureFlag = mockFeatureFlags[0]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders feature flags correctly', () => { + render(); + + expect(screen.getByText(mockFlag.name)).toBeInTheDocument(); + expect(screen.getByText(mockFlag.description)).toBeInTheDocument(); + expect(screen.getByText(mockFlag.status)).toBeInTheDocument(); + expect(screen.getByText(/Created:/)).toBeInTheDocument(); + expect(screen.getByText(/Updated:/)).toBeInTheDocument(); + }); + + it('applies correct status styling', () => { + render(); + + const statusElement = screen.getByText(mockFlag.status); + expect(statusElement).toHaveClass('bg-green-100', 'text-green-700'); + }); + + it('calls onCardClick with correct flag id when clicked', () => { + render(); + + fireEvent.click(screen.getByText(mockFlag.name)); + expect(mockOnCardClick).toHaveBeenCalledWith(mockFlag.id); + }); +}); diff --git a/tests/mocks/handlers.ts b/tests/mocks/handlers.ts new file mode 100644 index 0000000..27d2ab9 --- /dev/null +++ b/tests/mocks/handlers.ts @@ -0,0 +1,8 @@ +import { http, HttpResponse } from 'msw'; +import { mockFeatureFlags } from './mockData'; + +export const handlers = [ + http.get('http://localhost:8000/feature-flags', () => { + return HttpResponse.json(mockFeatureFlags, { status: 200 }); + }), +]; diff --git a/tests/mocks/mockData.ts b/tests/mocks/mockData.ts new file mode 100644 index 0000000..636dc64 --- /dev/null +++ b/tests/mocks/mockData.ts @@ -0,0 +1,30 @@ +import { FeatureFlag } from '../../src/services/api'; + +export const mockFeatureFlags: FeatureFlag[] = [ + { + id: '1', + name: 'Test Feature Flag', + description: 'This is a test feature flag', + status: 'ENABLED', + createdAt: 17095440000, + createdBy: 'test.user', + updatedAt: 17095440000, + updatedBy: 'test.user', + }, + { + id: '2', + name: 'Demo Feature Flag', + description: 'This is demo test flag', + status: 'DISABLED', + createdAt: 17094576000, + createdBy: 'test.user', + updatedAt: 17094576000, + updatedBy: 'test.user', + }, +]; + +export const featureFlagService = { + getFeatureFlags: async () => mockFeatureFlags, + getFeatureFlag: async (id: string) => + mockFeatureFlags.find((flag) => flag.id === id), +}; diff --git a/tests/mocks/server.ts b/tests/mocks/server.ts new file mode 100644 index 0000000..e52fee0 --- /dev/null +++ b/tests/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/tests/pages/FeatureFlagList.test.tsx b/tests/pages/FeatureFlagList.test.tsx new file mode 100644 index 0000000..8255d83 --- /dev/null +++ b/tests/pages/FeatureFlagList.test.tsx @@ -0,0 +1,83 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { BrowserRouter } from 'react-router-dom'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; +import FeatureFlagList from '../../src/pages/FeatureFlagList'; +// import { getAllFeatureFlags } from '../../src/services/api'; + +vi.mock('../../src/config', () => ({ + getConfig: () => ({ + featureFlagBaseUrl: 'http://localhost:8000', + }), +})); + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', () => ({ + ...require('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +describe('FeatureFlagList', () => { + afterEach(() => { + vi.clearAllMocks(); + server.resetHandlers(); + }); + + const renderComponent = () => { + render( + + + , + ); + }; + + it('renders loading spinner initially', () => { + renderComponent(); + expect(screen.getByTestId('spinner')).toBeInTheDocument(); + }); + + it('renders feature flags after loading', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.queryByTestId('spinner')).not.toBeInTheDocument(); + }); + + expect(screen.getByTestId('feature-flag-1')).toBeInTheDocument(); + expect(screen.getByTestId('feature-flag-2')).toBeInTheDocument(); + }); + + it('displays error message when API call fails', async () => { + server.use( + http.get('http://localhost:8000/feature-flags', () => { + return HttpResponse.json( + { message: 'Failed to fetch feature flags' }, + { status: 500 }, + ); + }), + ); + + renderComponent(); + + await waitFor(() => { + expect( + screen.getByText(/Failed to fetch feature flags/), + ).toBeInTheDocument(); + }); + }); + + it('navigates to feature flag detail page when card is clicked', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('feature-flag-1')).toBeInTheDocument(); + }); + + const flagCard = screen.getByTestId('feature-flag-1'); + await userEvent.click(flagCard); + + expect(mockNavigate).toHaveBeenCalledWith('/featureFlag/1'); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts index a379cee..98e1a27 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,5 +1,13 @@ import '@testing-library/jest-dom'; -import { vi } from 'vitest'; +import { vi, beforeAll, afterAll, afterEach } from 'vitest'; +import { server } from './mocks/server'; + +// eslint-disable-next-line +process.env.TZ = 'Asia/Kolkata'; +// MSW Setup +beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); class IntersectionObserverMock { observe = vi.fn(); diff --git a/tests/unit/format-timestamp.test.ts b/tests/unit/format-timestamp.test.ts new file mode 100644 index 0000000..b1c4122 --- /dev/null +++ b/tests/unit/format-timestamp.test.ts @@ -0,0 +1,10 @@ +import { formatTimestamp } from '../../src/utils/format-timestamp'; + +describe('formatTimestamp', () => { + it('should format timestamp correctly', () => { + const timestamp = 1718140371; + const formattedDate = formatTimestamp(timestamp); + + expect(formattedDate).toBe('June 12, 2024 at 02:42 AM'); + }); +});