diff --git a/crackcode/client/package-lock.json b/crackcode/client/package-lock.json index ec5fcc16..6a72ee6a 100644 --- a/crackcode/client/package-lock.json +++ b/crackcode/client/package-lock.json @@ -21,6 +21,9 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -28,9 +31,79 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", - "vite": "^7.2.4" + "jsdom": "^29.0.1", + "vite": "^7.2.4", + "vitest": "^4.1.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -266,6 +339,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -314,6 +397,161 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -887,6 +1125,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1339,6 +1595,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", @@ -1596,6 +1859,103 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1641,6 +2001,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1671,6 +2049,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -1703,70 +2082,213 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "tinyrainbow": "^3.0.3" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/argparse": { + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1801,6 +2323,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1891,6 +2423,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1991,6 +2533,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1998,6 +2561,20 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2016,6 +2593,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2032,6 +2616,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2041,6 +2635,13 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", @@ -2084,6 +2685,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2102,6 +2716,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2368,6 +2989,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "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" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2378,6 +3009,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2710,6 +3351,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2747,6 +3401,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2770,6 +3434,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2806,6 +3477,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3168,6 +3890,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3198,6 +3930,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3219,6 +3958,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3297,6 +4046,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3360,6 +4120,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3380,6 +4153,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3437,6 +4217,34 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3476,6 +4284,13 @@ "react": "^19.2.3" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3537,6 +4352,30 @@ "react-dom": "^18 || ^19" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3591,6 +4430,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3636,6 +4488,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3645,12 +4504,39 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", "license": "MIT" }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3677,6 +4563,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -3696,12 +4589,29 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3718,6 +4628,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3737,6 +4703,16 @@ "node": ">= 0.8.0" } }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3853,6 +4829,136 @@ } } }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3869,6 +4975,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3879,6 +5002,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/crackcode/client/package.json b/crackcode/client/package.json index a55580e8..ba35b57f 100644 --- a/crackcode/client/package.json +++ b/crackcode/client/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { "@monaco-editor/react": "^4.7.0", @@ -23,6 +24,9 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -30,6 +34,8 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", - "vite": "^7.2.4" + "jsdom": "^29.0.1", + "vite": "^7.2.4", + "vitest": "^4.1.0" } } diff --git a/crackcode/client/setupTests.js b/crackcode/client/setupTests.js new file mode 100644 index 00000000..7b7189a9 --- /dev/null +++ b/crackcode/client/setupTests.js @@ -0,0 +1,2 @@ +// setupTests.js +import '@testing-library/jest-dom'; \ No newline at end of file diff --git a/crackcode/client/src/components/store/StoreGrid.jsx b/crackcode/client/src/components/store/StoreGrid.jsx index a43e0a13..7831fd14 100644 --- a/crackcode/client/src/components/store/StoreGrid.jsx +++ b/crackcode/client/src/components/store/StoreGrid.jsx @@ -1,32 +1,3 @@ -// import StoreItemCard from "./StoreItemCard"; - -// export default function StoreGrid({ -// items = [], -// onBuyXP, -// onBuyPaid, -// buyingItemId, -// }) { -// if (!items.length) { -// return

No store items found.

; -// } - -// return ( -//
-// {items.map((item) => ( -// -// ))} -//
-// ); -// } - - - import StoreItemCard from "./StoreItemCard"; export default function StoreGrid({ diff --git a/crackcode/client/src/components/store/StoreItemCard.jsx b/crackcode/client/src/components/store/StoreItemCard.jsx index 6b073541..0a6735c8 100644 --- a/crackcode/client/src/components/store/StoreItemCard.jsx +++ b/crackcode/client/src/components/store/StoreItemCard.jsx @@ -1,142 +1,6 @@ -// import Card from "../ui/Card"; -// import Button from "../ui/Button"; -// import {CirclePoundSterling, CircleDollarSign} from "lucide-react"; - -// export default function StoreItemCard({ -// item, -// onBuyXP, -// onBuyPaid, -// loading = false, -// isInventoryView = false, -// onEquip, -// equippingItemId, -// equippedItemId, -// }) { -// const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5051"; - -// const rawImagePath = item.imageUrl || item.image || ""; - -// const normalizedImagePath = rawImagePath.startsWith("/upload/") -// ? rawImagePath.replace("/upload/", "/uploads/") -// : rawImagePath; - -// const imageSrc = normalizedImagePath -// ? normalizedImagePath.startsWith("http") -// ? normalizedImagePath -// : `${API_BASE_URL}${normalizedImagePath}` -// : "/placeholder.png"; - -// const pricingType = item.pricing?.type; -// const amount = item.pricing?.amount ?? 0; -// const currency = item.pricing?.currency ?? "USD"; - -// const category = (item.category || item.type || "item").toLowerCase(); - -// const isEquipping = equippingItemId === item._id; -// const isEquipped = equippedItemId === item._id || item.isEquipped; - -// let displayPrice = "N/A"; -// let buttonLabel = "Buy"; -// let buttonAction = () => {}; -// let isDisabled = loading; - -// if (!isInventoryView) { -// if (pricingType === "tokens") { - -// displayPrice = ( -//
-// {amount} -//
-// ); - -// buttonLabel = loading ? "Buying..." : "Buy"; -// buttonAction = () => onBuyXP?.(item._id); -// } else if (pricingType === "paid") { - -// displayPrice = ( -//
-// {amount} -//
-// ); - -// buttonLabel = loading ? "Redirecting..." : "Buy with Card"; -// buttonAction = () => onBuyPaid?.(item._id); -// } else if (pricingType === "free") { -// displayPrice = "Free"; -// buttonLabel = loading ? "Claiming..." : "Claim"; -// buttonAction = () => onBuyXP?.(item._id); -// } -// } else { -// displayPrice = ""; - -// if (isEquipped) { -// if (category === "theme") { -// buttonLabel = "Theme Applied"; -// } else if (category === "title") { -// buttonLabel = "Title Equipped"; -// } else { -// buttonLabel = "Avatar Equipped"; -// } -// isDisabled = true; -// buttonAction = () => {}; -// } else { -// if (category === "theme") { -// buttonLabel = isEquipping ? "Applying..." : "Apply Theme"; -// } else if (category === "title") { -// buttonLabel = isEquipping ? "Equipping..." : "Equip Title"; -// } else { -// buttonLabel = isEquipping ? "Equipping..." : "Equip Avatar"; -// } -// isDisabled = isEquipping; -// buttonAction = () => onEquip?.(item); -// } -// } - -// return ( -// -//
-// {item.name} { -// e.currentTarget.onerror = null; -// e.currentTarget.src = "/placeholder.png"; -// }} -// /> -//
- -//
-//

-// {item.category || item.type || "item"} -//

- -//

-// {item.name} -//

- -//
★★★★☆
- -//
-// {!isInventoryView ? ( -// {displayPrice} -// ) : ( -// Owned -// )} - -// -//
-//
-//
-// ); -// } - import Card from "../ui/Card"; import Button from "../ui/Button"; import { CirclePoundSterling, CircleDollarSign } from "lucide-react"; -import { useTheme } from "../../context/theme/ThemeContext"; export default function StoreItemCard({ item, @@ -148,8 +12,6 @@ export default function StoreItemCard({ equippingItemId, equippedItemId, }) { - const { theme } = useTheme(); - const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051"; const rawImagePath = item.imageUrl || item.image || ""; @@ -161,16 +23,12 @@ export default function StoreItemCard({ if (/^https?:\/\//i.test(p)) { imageSrc = p; } else if (p.startsWith("/upload/")) { - // legacy path: /upload/... -> normalize to /uploads/ and serve from API imageSrc = `${API_BASE_URL}${p.replace("/upload/", "/uploads/")}`; } else if (p.startsWith("/uploads") || p.includes("/uploads/")) { - // server-hosted uploads imageSrc = `${API_BASE_URL}${p.startsWith("/") ? p : `/${p}`}`; } else if (p.startsWith("/")) { - // client public path (Vite will serve this) imageSrc = p; } else { - // relative path from seeds or asset references -> use filename from path const parts = p.split(/[\\/]/); const filename = parts[parts.length - 1] || p; imageSrc = `/shop/${filename}`; @@ -184,85 +42,32 @@ export default function StoreItemCard({ const isEquipping = equippingItemId === item._id; const isEquipped = equippedItemId === item._id || item.isEquipped; - const isLightFamily = ["light", "cream", "country"].includes(theme); - - const cardClass = - theme === "light" - ? "border-gray-200 bg-gray-100 shadow-md hover:shadow-lg" - : theme === "cream" - ? "border-[#ddd2c1] bg-[#f3ede3] shadow-md hover:shadow-lg" - : theme === "country" - ? "border-[#cfbea6] bg-[#efe4d3] shadow-md hover:shadow-lg" - : theme === "midnight" - ? "border-[#22314f] bg-[#12203c] shadow-md hover:shadow-xl" - : "border-gray-800 bg-[#161616] shadow-md hover:shadow-xl"; - - const imageAreaClass = - theme === "light" - ? "bg-gray-50" - : theme === "cream" - ? "bg-[#faf6ef]" - : theme === "country" - ? "bg-[#f7efe2]" - : theme === "midnight" - ? "bg-[#0d1a33]" - : "bg-[#222222]"; - - const contentAreaClass = - theme === "light" - ? "bg-gray-100" - : theme === "cream" - ? "bg-[#f3ede3]" - : theme === "country" - ? "bg-[#efe4d3]" - : theme === "midnight" - ? "bg-[#0f1b35]" - : "bg-[#0f0f0f]"; - - const titleClass = isLightFamily ? "text-gray-900" : "text-white"; - const metaClass = - theme === "midnight" - ? "text-gray-300" - : isLightFamily - ? "text-gray-500" - : "text-gray-400"; - const ownedClass = - theme === "midnight" - ? "text-gray-300" - : isLightFamily - ? "text-gray-500" - : "text-gray-400"; - const ratingClass = "text-green-500"; - const priceClass = isLightFamily ? "text-green-600" : "text-green-400"; - let displayPrice = "N/A"; let buttonLabel = "Buy"; - let buttonAction = () => { }; + let buttonAction = () => {}; let isDisabled = loading; if (!isInventoryView) { if (pricingType === "tokens") { displayPrice = ( -
+
{amount}
); - buttonLabel = loading ? "Buying..." : "Buy"; buttonAction = () => onBuyXP?.(item._id); } else if (pricingType === "paid") { displayPrice = ( -
+
{amount}
); - buttonLabel = loading ? "Redirecting..." : "Buy with Card"; buttonAction = () => onBuyPaid?.(item._id); } else if (pricingType === "free") { - displayPrice = Free; + displayPrice = Free; buttonLabel = loading ? "Claiming..." : "Claim"; buttonAction = () => onBuyXP?.(item._id); } @@ -278,7 +83,7 @@ export default function StoreItemCard({ buttonLabel = "Avatar Equipped"; } isDisabled = true; - buttonAction = () => { }; + buttonAction = () => {}; } else { if (category === "theme") { buttonLabel = isEquipping ? "Applying..." : "Apply Theme"; @@ -297,9 +102,13 @@ export default function StoreItemCard({ variant="flat" padding="none" shadow="md" - className={`overflow-hidden rounded-2xl border transition hover:-translate-y-0.5 ${cardClass}`} + className="overflow-hidden rounded-2xl border transition hover:-translate-y-0.5" + style={{ background: 'var(--surface2)', borderColor: 'var(--border)' }} > -
+
{item.name}
-
-

+

+

{item.category || item.type || "item"}

-

+

{item.name}

-
★★★★☆
+
★★★★☆
{!isInventoryView ? (
{displayPrice}
) : ( - Owned + Owned )}
); -} \ No newline at end of file +} diff --git a/crackcode/client/src/components/store/StoreSidebar.jsx b/crackcode/client/src/components/store/StoreSidebar.jsx index 226ce255..aa042e68 100644 --- a/crackcode/client/src/components/store/StoreSidebar.jsx +++ b/crackcode/client/src/components/store/StoreSidebar.jsx @@ -1,76 +1,20 @@ -// export default function StoreSidebar({ category, setCategory }) { -// const categories = [ -// { label: "All", value: "all" }, -// { label: "Avatars", value: "avatar" }, -// { label: "Themes", value: "theme" }, -// { label: "Titles", value: "title" }, -// { label: "My Inventory", value: "inventory" }, -// ]; - -// return ( -//
-//

Categories

- -//
-// {categories.map((cat) => ( -// -// ))} -//
-//
-// ); -// } -import { useTheme } from "../../context/theme/ThemeContext"; - export default function StoreSidebar({ category, setCategory }) { - const { theme } = useTheme(); - const categories = [ { label: "All", value: "all" }, { label: "Avatars", value: "avatar" }, { label: "Themes", value: "theme" }, - { label: "My Inventory", value: "inventory" }, ]; - const isLightFamily = ["light", "cream", "country"].includes(theme); - - const sidebarClass = - theme === "light" - ? "bg-gray-100 border-gray-200" - : theme === "cream" - ? "bg-[#eee7db] border-[#ddd2c1]" - : theme === "country" - ? "bg-[#e7dccd] border-[#d4c4ad]" - : theme === "midnight" - ? "bg-[#0f1b35] border-[#1e2c4d]" - : "bg-[#111111] border-gray-800"; - - const headingClass = isLightFamily ? "text-gray-600" : "text-gray-400"; - - const idleButtonClass = - theme === "light" - ? "text-gray-700 hover:bg-gray-200" - : theme === "cream" - ? "text-gray-700 hover:bg-[#e6dccd]" - : theme === "country" - ? "text-gray-800 hover:bg-[#dccdb8]" - : theme === "midnight" - ? "text-gray-200 hover:bg-[#162544]" - : "text-gray-300 hover:bg-gray-800"; - return ( -
-

+
+

Categories

@@ -79,11 +23,22 @@ export default function StoreSidebar({ category, setCategory }) { @@ -91,4 +46,4 @@ export default function StoreSidebar({ category, setCategory }) {

); -} \ No newline at end of file +} diff --git a/crackcode/client/src/pages/shop/DetectiveStore.jsx b/crackcode/client/src/pages/shop/DetectiveStore.jsx index 273c0d96..2bb8c156 100644 --- a/crackcode/client/src/pages/shop/DetectiveStore.jsx +++ b/crackcode/client/src/pages/shop/DetectiveStore.jsx @@ -1,695 +1,3 @@ -// import { useEffect, useMemo, useState } from "react"; -// import StoreGrid from "../../components/store/StoreGrid"; -// import StoreSidebar from "../../components/store/StoreSidebar"; -// import Toast from "../../components/common/Toast"; -// import HQBtn from "../../components/common/HQBtn"; -// import BackBtn from "../../components/common/BackBtn"; - -// export default function DetectiveStore() { -// const [items, setItems] = useState([]); -// const [inventoryItems, setInventoryItems] = useState([]); -// const [ownedItemIds, setOwnedItemIds] = useState(new Set()); -// const [category, setCategory] = useState("all"); -// const [buyingItemId, setBuyingItemId] = useState(null); -// const [loading, setLoading] = useState(true); -// const [inventoryLoading, setInventoryLoading] = useState(false); -// const [equippingItemId, setEquippingItemId] = useState(null); -// const [equippedItemId, setEquippedItemId] = useState(null); - -// const [username, setUsername] = useState("User"); -// const [tokensRemaining, setTokensRemaining] = useState(0); -// const [profileImage, setProfileImage] = useState("/placeholder.png"); - -// const getToken = () => localStorage.getItem("accessToken"); - -// const [toast, setToast] = useState({ -// show: false, -// message: "", -// type: "success", -// }); - -// const showToast = (message, type = "success") => { -// setToast({ -// show: true, -// message, -// type, -// }); -// }; - -// const normalizeImageUrl = (src) => { -// if (!src) return "/placeholder.png"; -// if (src.startsWith("http")) return src; -// if (src.startsWith("/uploads")) return `http://localhost:5051${src}`; -// if (src.startsWith("/src")) return src; -// return src; -// }; - -// const loadProfile = async () => { -// try { -// const res = await fetch("http://localhost:5051/api/profile", { -// method: "GET", -// headers: { -// Authorization: `Bearer ${getToken()}`, -// }, -// }); - -// const data = await res.json(); -// console.log("PROFILE DATA:", data); - -// const user = -// data?.user || -// data?.profile || -// data?.data?.user || -// data?.data || -// data; - -// setUsername(user?.username || user?.name || "User"); -// setTokensRemaining(user?.tokens ?? 0); - -// const avatarSrc = -// user?.equippedAvatarItemId?.imageUrl || -// user?.avatar || -// "/placeholder.png"; - -// setProfileImage(normalizeImageUrl(avatarSrc)); -// } catch (error) { -// console.error("Failed to load profile:", error); -// setUsername("User"); -// setTokensRemaining(0); -// setProfileImage("/placeholder.png"); -// } -// }; - -// const loadInventory = async () => { -// try { -// const res = await fetch("http://localhost:5051/api/shop/inventory", { -// method: "GET", -// headers: { -// Authorization: `Bearer ${getToken()}`, -// }, -// }); - -// const data = await res.json(); -// const inventory = Array.isArray(data) ? data : data.items || []; - -// setInventoryItems(inventory); - -// const ids = new Set( -// inventory.map((inv) => String(inv.itemId?._id || inv.itemId || inv._id)) -// ); - -// setOwnedItemIds(ids); -// } catch (error) { -// console.error("Failed to load inventory:", error); -// } -// }; - -// useEffect(() => { -// const fetchItems = async () => { -// try { -// setLoading(true); - -// const res = await fetch("http://localhost:5051/api/shop/items"); -// const data = await res.json(); - -// setItems(Array.isArray(data) ? data : data.items || []); -// } catch (error) { -// console.error("Failed to fetch store items:", error); -// showToast("Failed to load store items", "error"); -// } finally { -// setLoading(false); -// } -// }; - -// fetchItems(); -// loadInventory(); -// loadProfile(); -// }, []); - -// useEffect(() => { -// if (category === "inventory") { -// const fetchInventoryTab = async () => { -// try { -// setInventoryLoading(true); -// await loadInventory(); -// } catch (error) { -// console.error("Failed to fetch inventory:", error); -// setInventoryItems([]); -// showToast("Failed to load inventory", "error"); -// } finally { -// setInventoryLoading(false); -// } -// }; - -// fetchInventoryTab(); -// } -// }, [category]); - -// const handleBuyTokens = async (itemId) => { -// try { -// setBuyingItemId(itemId); - -// const res = await fetch("http://localhost:5051/api/shop/purchase", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ itemId }), -// }); - -// const data = await res.json(); - -// if (data.success) { -// showToast("Successful! Item purchased", "success"); -// await loadInventory(); -// await loadProfile(); -// } else { -// showToast(data.message || "Purchase failed", "error"); -// } -// } catch (error) { -// console.error("Token purchase failed:", error); -// showToast("Purchase failed", "error"); -// } finally { -// setBuyingItemId(null); -// } -// }; - -// const handleBuyPaid = async (itemId) => { -// try { -// setBuyingItemId(itemId); - -// const res = await fetch("http://localhost:5051/api/shop/checkout", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ itemId }), -// }); - -// const data = await res.json(); - -// if (data?.url) { -// showToast("Redirecting to payment...", "success"); -// window.location.href = data.url; -// } else { -// showToast(data.message || "Stripe checkout failed", "error"); -// } -// } catch (error) { -// console.error("Stripe checkout failed:", error); -// showToast("Stripe checkout failed", "error"); -// } finally { -// setBuyingItemId(null); -// } -// }; - -// const handleEquip = async (item) => { -// try { -// setEquippingItemId(item._id); - -// const res = await fetch("http://localhost:5051/api/profile/equip-item", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ -// itemId: item._id, -// category: item.category, -// }), -// }); - -// const data = await res.json(); - -// if (data.success) { -// setEquippedItemId(item._id); -// showToast("Item equipped successfully!", "success"); -// await loadProfile(); -// } else { -// showToast(data.message || "Failed to equip item", "error"); -// } -// } catch (error) { -// console.error("Equip failed:", error); -// showToast("Failed to equip item", "error"); -// } finally { -// setEquippingItemId(null); -// } -// }; - -// const displayedItems = useMemo(() => { -// if (category === "inventory") { -// return inventoryItems.map((inv) => inv.itemId || inv); -// } - -// if (category === "all") { -// return items; -// } - -// return items.filter( -// (item) => item.category?.toLowerCase() === category.toLowerCase() -// ); -// }, [category, items, inventoryItems]); - -// const sectionTitle = -// category === "inventory" -// ? "My Inventory" -// : category === "all" -// ? "All Items" -// : `${category.charAt(0).toUpperCase() + category.slice(1)} Items`; - -// return ( -//
-// setToast((prev) => ({ ...prev, show: false }))} -// /> - -//
-//
-// -// -//
- -//
-// {username} { -// e.currentTarget.src = "/placeholder.png"; -// }} -// /> - -//
-//

{username}

-//

-// {tokensRemaining} Tokens -//

-//
-//
-//
- -//
-// - -//
-//

Detective Store

-//

-// Unlock exclusive avatars, themes, and titles to customize your -// detective profile -//

- -//

{sectionTitle}

- -// {loading && category !== "inventory" && ( -//

Loading store items...

-// )} - -// {inventoryLoading && category === "inventory" && ( -//

Loading your inventory...

-// )} - -// {!loading && !inventoryLoading && displayedItems.length === 0 && ( -//

-// {category === "inventory" -// ? "You do not own any items yet." -// : "No items found in this category."} -//

-// )} - -// {!loading && !inventoryLoading && displayedItems.length > 0 && ( -// -// )} -//
-//
-//
-// ); -// } - -// -------------------------------------------------------------------------------------------------- - -// import { useEffect, useMemo, useState } from "react"; -// import StoreGrid from "../../components/store/StoreGrid"; -// import StoreSidebar from "../../components/store/StoreSidebar"; -// import Toast from "../../components/common/Toast"; -// import HQBtn from "../../components/common/HQBtn"; -// import BackBtn from "../../components/common/BackBtn"; - - -// export default function DetectiveStore() { -// const [items, setItems] = useState([]); -// const [inventoryItems, setInventoryItems] = useState([]); -// const [ownedItemIds, setOwnedItemIds] = useState(new Set()); -// const [category, setCategory] = useState("all"); -// const [buyingItemId, setBuyingItemId] = useState(null); -// const [loading, setLoading] = useState(true); -// const [inventoryLoading, setInventoryLoading] = useState(false); -// const [equippingItemId, setEquippingItemId] = useState(null); -// const [equippedItemId, setEquippedItemId] = useState(null); - -// const [username, setUsername] = useState("User"); -// const [tokensRemaining, setTokensRemaining] = useState(0); -// const [profileImage, setProfileImage] = useState("/placeholder.png"); - -// const getToken = () => localStorage.getItem("accessToken"); - -// const [toast, setToast] = useState({ -// show: false, -// message: "", -// type: "success", -// }); - -// const showToast = (message, type = "success") => { -// setToast({ -// show: true, -// message, -// type, -// }); -// }; - -// const normalizeImageUrl = (src) => { -// if (!src) return "/placeholder.png"; -// if (src.startsWith("http")) return src; -// if (src.startsWith("/uploads")) return `http://localhost:5051${src}`; -// if (src.startsWith("/src")) return src; -// return src; -// }; - -// const loadProfile = async () => { -// try { -// const res = await fetch("http://localhost:5051/api/profile", { -// method: "GET", -// headers: { -// Authorization: `Bearer ${getToken()}`, -// }, -// }); - -// const data = await res.json(); -// console.log("PROFILE DATA:", data); - -// const user = -// data?.user || -// data?.profile || -// data?.data?.user || -// data?.data || -// data; - -// setUsername(user?.username || user?.name || "User"); -// setTokensRemaining(user?.tokens ?? 0); - -// const avatarSrc = -// user?.equippedAvatarItemId?.imageUrl || -// user?.avatar || -// "/placeholder.png"; - -// setProfileImage(normalizeImageUrl(avatarSrc)); -// } catch (error) { -// console.error("Failed to load profile:", error); -// setUsername("User"); -// setTokensRemaining(0); -// setProfileImage("/placeholder.png"); -// } -// }; - -// const loadInventory = async () => { -// try { -// const res = await fetch("http://localhost:5051/api/shop/inventory", { -// method: "GET", -// headers: { -// Authorization: `Bearer ${getToken()}`, -// }, -// }); - -// const data = await res.json(); -// const inventory = Array.isArray(data) ? data : data.items || []; - -// setInventoryItems(inventory); - -// const ids = new Set( -// inventory.map((inv) => String(inv.itemId?._id || inv.itemId || inv._id)) -// ); - -// setOwnedItemIds(ids); -// } catch (error) { -// console.error("Failed to load inventory:", error); -// } -// }; - -// useEffect(() => { -// const fetchItems = async () => { -// try { -// setLoading(true); - -// const res = await fetch("http://localhost:5051/api/shop/items"); -// const data = await res.json(); - -// setItems(Array.isArray(data) ? data : data.items || []); -// } catch (error) { -// console.error("Failed to fetch store items:", error); -// showToast("Failed to load store items", "error"); -// } finally { -// setLoading(false); -// } -// }; - -// fetchItems(); -// loadInventory(); -// loadProfile(); -// }, []); - -// useEffect(() => { -// if (category === "inventory") { -// const fetchInventoryTab = async () => { -// try { -// setInventoryLoading(true); -// await loadInventory(); -// } catch (error) { -// console.error("Failed to fetch inventory:", error); -// setInventoryItems([]); -// showToast("Failed to load inventory", "error"); -// } finally { -// setInventoryLoading(false); -// } -// }; - -// fetchInventoryTab(); -// } -// }, [category]); - -// const handleBuyTokens = async (itemId) => { -// try { -// setBuyingItemId(itemId); - -// const res = await fetch("http://localhost:5051/api/shop/purchase", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ itemId }), -// }); - -// const data = await res.json(); - -// if (data.success) { -// showToast("Successful! Item purchased", "success"); -// await loadInventory(); -// await loadProfile(); -// } else { -// showToast(data.message || "Purchase failed", "error"); -// } -// } catch (error) { -// console.error("Token purchase failed:", error); -// showToast("Purchase failed", "error"); -// } finally { -// setBuyingItemId(null); -// } -// }; - -// const handleBuyPaid = async (itemId) => { -// try { -// setBuyingItemId(itemId); - -// const res = await fetch("http://localhost:5051/api/shop/checkout", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ itemId }), -// }); - -// const data = await res.json(); - -// if (data?.url) { -// showToast("Redirecting to payment...", "success"); -// window.location.href = data.url; -// } else { -// showToast(data.message || "Stripe checkout failed", "error"); -// } -// } catch (error) { -// console.error("Stripe checkout failed:", error); -// showToast("Stripe checkout failed", "error"); -// } finally { -// setBuyingItemId(null); -// } -// }; - -// const handleEquip = async (item) => { -// try { -// setEquippingItemId(item._id); - -// const res = await fetch("http://localhost:5051/api/profile/equip-item", { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// Authorization: `Bearer ${getToken()}`, -// }, -// body: JSON.stringify({ -// itemId: item._id, -// category: item.category, -// }), -// }); - -// const data = await res.json(); - -// if (data.success) { -// setEquippedItemId(item._id); -// showToast("Item equipped successfully!", "success"); -// await loadProfile(); -// } else { -// showToast(data.message || "Failed to equip item", "error"); -// } -// } catch (error) { -// console.error("Equip failed:", error); -// showToast("Failed to equip item", "error"); -// } finally { -// setEquippingItemId(null); -// } -// }; - -// const displayedItems = useMemo(() => { -// if (category === "inventory") { -// return inventoryItems.map((inv) => inv.itemId || inv); -// } - -// if (category === "all") { -// return items; -// } - -// return items.filter( -// (item) => item.category?.toLowerCase() === category.toLowerCase() -// ); -// }, [category, items, inventoryItems]); - -// const sectionTitle = -// category === "inventory" -// ? "My Inventory" -// : category === "all" -// ? "All Items" -// : `${category.charAt(0).toUpperCase() + category.slice(1)} Items`; - -// return ( - -//
-// setToast((prev) => ({ ...prev, show: false }))} -// /> - -//
-//
-// -// -//
- -//
-// {username} { -// e.currentTarget.src = "/placeholder.png"; -// }} -// /> - -//
-//

{username}

-//

-// {tokensRemaining} Tokens -//

-//
-//
-//
- -//
-// - -//
-//

-// Detective Store -//

-//

-// Unlock exclusive avatars, themes, and titles to customize your -// detective profile -//

- -//

-// {sectionTitle} -//

- -// {loading && category !== "inventory" && ( -//

Loading store items...

-// )} - -// {inventoryLoading && category === "inventory" && ( -//

Loading your inventory...

-// )} - -// {!loading && !inventoryLoading && displayedItems.length === 0 && ( -//

-// {category === "inventory" -// ? "You do not own any items yet." -// : "No items found in this category."} -//

-// )} - -// {!loading && !inventoryLoading && displayedItems.length > 0 && ( -// -// )} -//
-//
-//
-// ); -// } - - -//-------------------------------------------------------------------------------------------------- import { useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import StoreGrid from "../../components/store/StoreGrid"; @@ -720,22 +28,6 @@ export default function DetectiveStore() { const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || "http://localhost:5051"; - const isLightFamily = ["light", "cream", "country"].includes(theme); - - const pageClass = - theme === "light" - ? "bg-gray-50 text-gray-900" - : theme === "cream" - ? "bg-[#f6f1e7] text-gray-900" - : theme === "country" - ? "bg-[#efe7dc] text-gray-900" - : theme === "midnight" - ? "bg-[#08142b] text-white" - : "bg-black text-white"; - - const titleClass = isLightFamily ? "text-gray-900" : "text-white"; - const subTextClass = isLightFamily ? "text-gray-500" : "text-gray-400"; - const tokenClass = isLightFamily ? "text-green-600" : "text-green-400"; const getToken = () => localStorage.getItem("accessToken"); @@ -790,6 +82,15 @@ export default function DetectiveStore() { "/placeholder.png"; setProfileImage(normalizeImageUrl(avatarSrc)); + + const equippedId = + user?.equippedAvatarItemId?._id || + user?.equippedThemeItemId?._id || + user?.equippedTitleItemId?._id || + null; + if (equippedId) { + setEquippedItemId(String(equippedId)); + } } catch (error) { console.error("Failed to load profile:", error); setUsername("User"); @@ -1101,23 +402,21 @@ export default function DetectiveStore() { : `${category.charAt(0).toUpperCase() + category.slice(1)} Items`; return ( -
-
-
-
+
+
-
+
{/* Heading row with avatar on the right */}
-

+

Detective Store

-
+
{username} { e.currentTarget.src = "/placeholder.png"; }} />
- + {tokensRemaining} Tokens
-

+

Unlock exclusive avatars, themes, and titles to customize your detective profile

-

+

{sectionTitle}

{loading && category !== "inventory" && ( -

Loading store items...

+

Loading store items...

)} {inventoryLoading && category === "inventory" && ( -

Loading your inventory...

+

Loading your inventory...

)} {!loading && !inventoryLoading && displayedItems.length === 0 && ( -

+

{category === "inventory" ? "You do not own any items yet." : "No items found in this category."} diff --git a/crackcode/client/src/pages/userprofile/userprofile.jsx b/crackcode/client/src/pages/userprofile/userprofile.jsx index 38bb3e63..518d079b 100644 --- a/crackcode/client/src/pages/userprofile/userprofile.jsx +++ b/crackcode/client/src/pages/userprofile/userprofile.jsx @@ -79,7 +79,8 @@ const UserProfile = () => { // Use server-provided fields: totalXP and tokens totalXP: data.totalXP ?? data.xp ?? 0, tokens: data.tokens ?? 0, - rank: "#" + (data.rank || "--") + rank: "#" + (data.rank || "--"), + avatar: data.avatar || prevData.avatar || "" })); } } catch (error) { @@ -254,8 +255,16 @@ const UserProfile = () => {

{/* User Avatar Circle - Shows user's profile picture or placeholder */} -
- {userStatus.avatar || '👤'} +
+ {(() => { + const raw = userStatus.avatar; + if (!raw) return '👤'; + const API_BASE = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL || 'http://localhost:5051'; + const src = raw.startsWith('http') ? raw : raw.startsWith('/uploads') ? `${API_BASE}${raw}` : raw.startsWith('/') ? raw : null; + return src + ? avatar { e.currentTarget.style.display = 'none'; e.currentTarget.parentElement.textContent = '👤'; }} /> + : raw; + })()}
{/* User Name and Rank Info */} diff --git a/crackcode/client/tests/AIAssistantAgent.test.jsx b/crackcode/client/tests/AIAssistantAgent.test.jsx new file mode 100644 index 00000000..42237d85 --- /dev/null +++ b/crackcode/client/tests/AIAssistantAgent.test.jsx @@ -0,0 +1,465 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +// jsdom does not implement scrollIntoView — stub it globally +window.HTMLElement.prototype.scrollIntoView = vi.fn(); + +import { EditorProvider, useEditor } from '../src/context/codeEditor/EditorContext'; +import AIAssistantChat from '../src/components/codeEditor/AIAssistantChat'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Renders AIAssistantChat inside a real EditorProvider. + * `overrides` are applied via a bridge component so individual tests can + * pre-seed context state (aiMessages, aiInput, isAiTyping, code, language, …). + */ +const OverrideBridge = ({ children, overrides }) => { + const ctx = useEditor(); + React.useEffect(() => { + Object.entries(overrides).forEach(([key, value]) => { + const setter = ctx[`set${key.charAt(0).toUpperCase()}${key.slice(1)}`]; + if (typeof setter === 'function') setter(value); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return children; +}; + +const renderChat = (overrides = {}) => + render( + + + + + + ); + +/** Returns a resolved fetch mock with the given reply text. */ +const mockFetchSuccess = (reply = 'Here is the answer.') => + vi.fn(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ success: true, data: { reply } }), + }) + ); + +/** Returns a resolved fetch mock that the server marks as error. */ +const mockFetchServerError = (message = 'Something went wrong') => + vi.fn(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ success: false, message }), + }) + ); + +/** Returns a rejected fetch mock (network failure). */ +const mockFetchNetworkError = (msg = 'Network error') => + vi.fn(() => Promise.reject(new Error(msg))); + +/** Returns a non-ok HTTP response mock. */ +const mockFetchHttpError = (status = 500, body = 'Internal Server Error') => + vi.fn(() => + Promise.resolve({ + ok: false, + status, + text: () => Promise.resolve(body), + }) + ); + +// ═════════════════════════════════════════════════════════════════════════════ +// 1. Initial render — welcome / empty state +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — initial render', () => { + it('shows the Detective AI welcome message when there are no messages', () => { + renderChat(); + expect( + screen.getByText(/I'm your AI partner on this case/i) + ).toBeInTheDocument(); + }); + + it('shows the "Run your code first" hint in the welcome message', () => { + renderChat(); + expect(screen.getByText(/Run your code first/i)).toBeInTheDocument(); + }); + + it('renders all four suggestion chips when no messages exist', () => { + renderChat(); + ['Explain my error', 'Give me a hint', 'Check complexity', 'Help with loops'].forEach( + (chip) => expect(screen.getByRole('button', { name: chip })).toBeInTheDocument() + ); + }); + + it('renders the textarea with the correct placeholder', () => { + renderChat(); + expect(screen.getByPlaceholderText('Ask the Detective AI…')).toBeInTheDocument(); + }); + + it('the send button is disabled when input is empty', () => { + renderChat(); + // The send button has no accessible name — query by its parent label context + const textarea = screen.getByPlaceholderText('Ask the Detective AI…'); + expect(textarea.value).toBe(''); + // The button closest to the textarea is the send button + const sendBtn = textarea.closest('div').querySelector('button'); + expect(sendBtn).toBeDisabled(); + }); + + it('shows the keyboard hint text', () => { + renderChat(); + expect(screen.getByText(/Enter to send/i)).toBeInTheDocument(); + expect(screen.getByText(/Shift\+Enter for new line/i)).toBeInTheDocument(); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 2. Input interaction +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — input interaction', () => { + it('updates the textarea as the user types', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'What is wrong?'); + expect(input.value).toBe('What is wrong?'); + }); + + it('enables the send button once text is entered', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Hello'); + const sendBtn = input.closest('div').querySelector('button'); + expect(sendBtn).not.toBeDisabled(); + }); + + it('send button remains disabled when input is only whitespace', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, ' '); + const sendBtn = input.closest('div').querySelector('button'); + expect(sendBtn).toBeDisabled(); + }); + + it('clicking a suggestion chip populates the input', async () => { + renderChat(); + await userEvent.click(screen.getByRole('button', { name: 'Give me a hint' })); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + expect(input.value).toBe('Give me a hint'); + }); + + it('clicking a suggestion chip focuses the input', async () => { + renderChat(); + await userEvent.click(screen.getByRole('button', { name: 'Explain my error' })); + expect(screen.getByPlaceholderText('Ask the Detective AI…')).toHaveFocus(); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 3. Sending a message — successful API response +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — sending a message (success)', () => { + beforeEach(() => { + vi.stubGlobal('fetch', mockFetchSuccess('Use a for-loop here.')); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('appends the user message to the chat after sending', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'How do I fix this?'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText('How do I fix this?')).toBeInTheDocument() + ); + }); + + it('clears the input after sending', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Question?'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => expect(input.value).toBe('')); + }); + + it('shows the assistant reply prefixed with the detective emoji', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Help'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText(/Use a for-loop here\./)).toBeInTheDocument() + ); + }); + + it('hides the welcome message after the first message is sent', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Test'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.queryByText(/I'm your AI partner on this case/i)).not.toBeInTheDocument() + ); + }); + + it('hides the suggestion chips after the first message is sent', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Test'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.queryByRole('button', { name: 'Give me a hint' })).not.toBeInTheDocument() + ); + }); + + it('sends Enter key to submit the message', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Keyboard send'); + await userEvent.keyboard('{Enter}'); + + await waitFor(() => + expect(screen.getByText('Keyboard send')).toBeInTheDocument() + ); + }); + + it('Shift+Enter does NOT submit the message', async () => { + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'No send'); + // Shift+Enter should add a newline, not submit + await userEvent.keyboard('{Shift>}{Enter}{/Shift}'); + + // No user message bubble should appear yet + expect(screen.queryByText('No send')).toBeInTheDocument(); // still in input + expect(fetch).not.toHaveBeenCalled(); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 4. Sending a message — API error handling +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — error handling', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('shows an error reply on network failure', async () => { + vi.stubGlobal('fetch', mockFetchNetworkError('Network error')); + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Ping'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText(/Error: Network error/)).toBeInTheDocument() + ); + }); + + it('shows an error reply on HTTP 500 response', async () => { + vi.stubGlobal('fetch', mockFetchHttpError(500, 'Internal Server Error')); + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Ping'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText(/Error:.*500/)).toBeInTheDocument() + ); + }); + + it('shows an error reply when success is false in the response body', async () => { + vi.stubGlobal('fetch', mockFetchServerError('Model unavailable')); + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Ping'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText(/Error: Model unavailable/)).toBeInTheDocument() + ); + }); + + it('error reply is prefixed with the detective emoji', async () => { + vi.stubGlobal('fetch', mockFetchNetworkError('Timeout')); + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Ping'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => + expect(screen.getByText(/🕵️.*Error:/)).toBeInTheDocument() + ); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 5. Typing indicator +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — typing indicator', () => { + it('shows the typing indicator while isAiTyping is true', () => { + renderChat({ isAiTyping: true }); + // The indicator renders 3 bouncing dots inside a flex container; + // the parent assistant bubble is present — verify the animated dots exist + const dots = document.querySelectorAll('.animate-bounce'); + expect(dots.length).toBe(3); + }); + + it('does not show the typing indicator when isAiTyping is false', () => { + renderChat({ isAiTyping: false }); + const dots = document.querySelectorAll('.animate-bounce'); + expect(dots.length).toBe(0); + }); + + it('send button is disabled while isAiTyping is true even with text', () => { + renderChat({ isAiTyping: true, aiInput: 'some text' }); + // The component reads aiInput from context, so seed it via overrides + const sendBtn = document.querySelector('button[disabled]'); + expect(sendBtn).toBeInTheDocument(); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 6. Message history rendering +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — message history rendering', () => { + const existingMessages = [ + { role: 'user', text: 'What is a loop?', id: 1 }, + { role: 'assistant', text: '🕵️ A loop repeats code.', id: 2 }, + { role: 'user', text: 'Thanks!', id: 3 }, + ]; + + it('renders pre-existing user messages', () => { + renderChat({ aiMessages: existingMessages }); + expect(screen.getByText('What is a loop?')).toBeInTheDocument(); + expect(screen.getByText('Thanks!')).toBeInTheDocument(); + }); + + it('renders pre-existing assistant messages', () => { + renderChat({ aiMessages: existingMessages }); + expect(screen.getByText('🕵️ A loop repeats code.')).toBeInTheDocument(); + }); + + it('does not show welcome message when aiMessages is non-empty', () => { + renderChat({ aiMessages: existingMessages }); + expect(screen.queryByText(/I'm your AI partner on this case/i)).not.toBeInTheDocument(); + }); + + it('does not show suggestion chips when aiMessages is non-empty', () => { + renderChat({ aiMessages: existingMessages }); + expect(screen.queryByRole('button', { name: 'Explain my error' })).not.toBeInTheDocument(); + }); + + it('renders all messages in the correct order', () => { + renderChat({ aiMessages: existingMessages }); + const texts = screen + .getAllByText(/What is a loop\?|🕵️ A loop repeats code\.|Thanks!/); + expect(texts[0]).toHaveTextContent('What is a loop?'); + expect(texts[1]).toHaveTextContent('🕵️ A loop repeats code.'); + expect(texts[2]).toHaveTextContent('Thanks!'); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 7. API request payload +// ═════════════════════════════════════════════════════════════════════════════ + +describe('AIAssistantChat — API request payload', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('sends question, language, and code to /api/ai/assistant', async () => { + const mockFetch = mockFetchSuccess('Answer'); + vi.stubGlobal('fetch', mockFetch); + + renderChat({ code: 'print("hi")', language: 'python' }); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Explain please'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledOnce()); + + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toBe('/api/ai/assistant'); + expect(options.method).toBe('POST'); + + const body = JSON.parse(options.body); + expect(body.question).toBe('Explain please'); + expect(body.language).toBe('python'); + expect(body.code).toBe('print("hi")'); + }); + + it('falls back to "python" when language is not set', async () => { + const mockFetch = mockFetchSuccess('Answer'); + vi.stubGlobal('fetch', mockFetch); + + // EditorContext defaults language to 'python', so this just verifies no crash + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Hello'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledOnce()); + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(body.language).toBe('python'); + }); + + it('includes the first testResult as lastJudgeResult', async () => { + const mockFetch = mockFetchSuccess('Answer'); + vi.stubGlobal('fetch', mockFetch); + + const testResults = [ + { status: 'failed', error: 'NameError', details: {} }, + { status: 'passed', details: {} }, + ]; + + renderChat({ testResults }); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Why did it fail?'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledOnce()); + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(body.lastJudgeResult).toEqual(testResults[0]); + }); + + it('sends credentials: include with every request', async () => { + const mockFetch = mockFetchSuccess('Answer'); + vi.stubGlobal('fetch', mockFetch); + + renderChat(); + const input = screen.getByPlaceholderText('Ask the Detective AI…'); + await userEvent.type(input, 'Hi'); + await userEvent.click(input.closest('div').querySelector('button')); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledOnce()); + expect(mockFetch.mock.calls[0][1].credentials).toBe('include'); + }); + + it('does not send when input is empty', async () => { + const mockFetch = mockFetchSuccess('Answer'); + vi.stubGlobal('fetch', mockFetch); + + renderChat(); + // Try pressing Enter without any text + await userEvent.keyboard('{Enter}'); + + expect(mockFetch).not.toHaveBeenCalled(); + }); +}); diff --git a/crackcode/client/tests/Authentication.test.jsx b/crackcode/client/tests/Authentication.test.jsx new file mode 100644 index 00000000..75ad9de2 --- /dev/null +++ b/crackcode/client/tests/Authentication.test.jsx @@ -0,0 +1,524 @@ +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { vi } from "vitest"; + +// ─── Shared mocks ──────────────────────────────────────────────────────────── + +vi.mock("react-toastify", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + }, +})); + +const { mockNavigate } = vi.hoisted(() => ({ mockNavigate: vi.fn() })); + +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); + +// Mock axios used inside auth source files +vi.mock("axios", async () => { + const actual = await vi.importActual("axios"); + return { + ...actual, + default: { + ...actual.default, + get: vi.fn(), + post: vi.fn(), + defaults: { + withCredentials: false, + headers: { common: {} }, + }, + }, + }; +}); + +// Mock SVG/PNG asset imports so Vite doesn't choke in jsdom +vi.mock("../src/assets/logo/crackcode_logo.svg", () => ({ default: "logo.svg" })); +vi.mock("../src/assets/logo/logo_dark.png", () => ({ default: "logo_dark.png" })); + +// Mock the Header and Button UI components used in EmailVerify +vi.mock("../src/components/common/Header", () => ({ + default: () =>
, +})); +vi.mock("../src/components/ui/Button", () => ({ + default: ({ children, ...props }) => , +})); + +// ─── Auth context mock factory ──────────────────────────────────────────────── + +import { AppContent } from "../src/context/userauth/authenticationContext"; +import React from "react"; + +const buildAuthContext = (overrides = {}) => ({ + backendUrl: "http://localhost:5051", + isLoggedIn: false, + setIsLoggedIn: vi.fn(), + userData: false, + setUserData: vi.fn(), + getUserData: vi.fn(), + setAuthHeader: vi.fn(), + isLoading: false, + ...overrides, +}); + +const renderWithAuth = (ui, contextValue, { route = "/" } = {}) => + render( + + {ui} + + ); + +// ═════════════════════════════════════════════════════════════════════════════ +// 1. Login Component +// ═════════════════════════════════════════════════════════════════════════════ +import Login from "../src/pages/userauth/Login"; +import axios from "axios"; + +describe("Login Component", () => { + let ctx; + + beforeEach(() => { + vi.clearAllMocks(); + ctx = buildAuthContext(); + Storage.prototype.getItem = vi.fn(() => null); + Storage.prototype.setItem = vi.fn(); + }); + + // ── Rendering ─────────────────────────────────────────────────────────── + + it("renders the login form by default", () => { + renderWithAuth(, ctx); + + expect(screen.getByPlaceholderText(/email address/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/^password$/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /sign in/i })).toBeInTheDocument(); + }); + + it("shows the 'Forgot password?' link in login mode", () => { + renderWithAuth(, ctx); + expect(screen.getByText(/forgot password/i)).toBeInTheDocument(); + }); + + // ── Tab switching ──────────────────────────────────────────────────────── + + it("switches to Sign Up form when the Sign Up tab is clicked", async () => { + renderWithAuth(, ctx); + + const signUpTab = screen.getByRole("button", { name: /sign up/i }); + fireEvent.click(signUpTab); + + expect(await screen.findByPlaceholderText(/full name/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/confirm password/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /create account/i })).toBeInTheDocument(); + }); + + it("hides name and confirm-password fields when switching back to Login", async () => { + renderWithAuth(, ctx); + + fireEvent.click(screen.getByRole("button", { name: /sign up/i })); + fireEvent.click(screen.getByRole("button", { name: /log in/i })); + + expect(screen.queryByPlaceholderText(/full name/i)).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText(/confirm password/i)).not.toBeInTheDocument(); + }); + + // ── Login submission ───────────────────────────────────────────────────── + + it("calls login API and navigates to /home on successful login", async () => { + axios.post.mockResolvedValueOnce({ + data: { + success: true, + accessToken: "test-token", + user: { isAccountVerified: true }, + }, + }); + ctx.getUserData.mockResolvedValueOnce(); + + renderWithAuth(, ctx); + + await userEvent.type(screen.getByPlaceholderText(/email address/i), "user@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "password123"); + fireEvent.click(screen.getByRole("button", { name: /sign in/i })); + + await waitFor(() => { + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/login"), + expect.objectContaining({ email: "user@test.com", password: "password123" }) + ); + expect(ctx.setIsLoggedIn).toHaveBeenCalledWith(true); + expect(mockNavigate).toHaveBeenCalledWith("/home"); + }); + }); + + it("redirects to /verify-account when user is not verified after login", async () => { + axios.post + .mockResolvedValueOnce({ + data: { + success: true, + accessToken: "test-token", + user: { isAccountVerified: false }, + }, + }) + .mockResolvedValueOnce({ data: {} }); // send-verify-otp call + + ctx.getUserData.mockResolvedValueOnce(); + + renderWithAuth(, ctx); + + await userEvent.type(screen.getByPlaceholderText(/email address/i), "user@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "password123"); + fireEvent.click(screen.getByRole("button", { name: /sign in/i })); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith("/verify-account"); + }); + }); + + it("shows an error toast when login API returns success: false", async () => { + axios.post.mockResolvedValueOnce({ + data: { success: false, message: "Invalid credentials" }, + }); + + const { toast } = await import("react-toastify"); + renderWithAuth(, ctx); + + await userEvent.type(screen.getByPlaceholderText(/email address/i), "bad@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "wrongpass"); + fireEvent.click(screen.getByRole("button", { name: /sign in/i })); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Invalid credentials"); + }); + }); + + // ── Registration submission ────────────────────────────────────────────── + + it("shows error when passwords do not match during sign-up", async () => { + const { toast } = await import("react-toastify"); + renderWithAuth(, ctx); + + fireEvent.click(screen.getByRole("button", { name: /sign up/i })); + + await userEvent.type(screen.getByPlaceholderText(/full name/i), "Alice"); + await userEvent.type(screen.getByPlaceholderText(/email address/i), "alice@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "pass1234"); + await userEvent.type(screen.getByPlaceholderText(/confirm password/i), "different"); + + // accept T&C + fireEvent.click(screen.getByRole("checkbox")); + + fireEvent.click(screen.getByRole("button", { name: /create account/i })); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Passwords do not match!"); + }); + }); + + it("shows error when T&C not accepted during sign-up", async () => { + const { toast } = await import("react-toastify"); + renderWithAuth(, ctx); + + fireEvent.click(screen.getByRole("button", { name: /sign up/i })); + + await userEvent.type(screen.getByPlaceholderText(/full name/i), "Bob"); + await userEvent.type(screen.getByPlaceholderText(/email address/i), "bob@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "pass1234"); + await userEvent.type(screen.getByPlaceholderText(/confirm password/i), "pass1234"); + // intentionally NOT checking T&C + + // The submit button is disabled when T&C is unchecked, so submit the form directly + fireEvent.submit(document.querySelector("form")); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + "You must accept the Terms and Conditions." + ); + }); + }); + + it("calls register API and navigates to /verify-account on successful sign-up", async () => { + axios.post.mockResolvedValueOnce({ + data: { success: true }, + }); + + const { toast } = await import("react-toastify"); + renderWithAuth(, ctx); + + fireEvent.click(screen.getByRole("button", { name: /sign up/i })); + + await userEvent.type(screen.getByPlaceholderText(/full name/i), "Carol"); + await userEvent.type(screen.getByPlaceholderText(/email address/i), "carol@test.com"); + await userEvent.type(screen.getByPlaceholderText(/^password$/i), "pass1234"); + await userEvent.type(screen.getByPlaceholderText(/confirm password/i), "pass1234"); + fireEvent.click(screen.getByRole("checkbox")); + fireEvent.click(screen.getByRole("button", { name: /create account/i })); + + await waitFor(() => { + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/register"), + expect.objectContaining({ name: "Carol", email: "carol@test.com" }) + ); + expect(toast.success).toHaveBeenCalledWith("OTP sent to your email."); + expect(mockNavigate).toHaveBeenCalledWith( + "/verify-account", + expect.objectContaining({ state: { email: "carol@test.com" } }) + ); + }); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 2. ProtectedRoute Component +// ═════════════════════════════════════════════════════════════════════════════ +import ProtectedRoute from "../src/components/common/ProtectedRoute"; + +describe("ProtectedRoute Component", () => { + const ChildComponent = () =>
Protected Content
; + + it("renders children when user is logged in and verified", () => { + const ctx = buildAuthContext({ + isLoggedIn: true, + userData: { isAccountVerified: true }, + }); + + renderWithAuth( + + + , + ctx + ); + + expect(screen.getByText("Protected Content")).toBeInTheDocument(); + }); + + it("redirects to /login when user is not logged in", () => { + const ctx = buildAuthContext({ isLoggedIn: false }); + + const { container } = renderWithAuth( + + + , + ctx, + { route: "/home" } + ); + + expect(screen.queryByText("Protected Content")).not.toBeInTheDocument(); + }); + + it("redirects to /verify-account when logged in but email unverified", () => { + const ctx = buildAuthContext({ + isLoggedIn: true, + userData: { isAccountVerified: false }, + }); + + renderWithAuth( + + + , + ctx, + { route: "/home" } + ); + + expect(screen.queryByText("Protected Content")).not.toBeInTheDocument(); + }); + + it("renders children when requireVerified is false even if email not verified", () => { + const ctx = buildAuthContext({ + isLoggedIn: true, + userData: { isAccountVerified: false }, + }); + + renderWithAuth( + + + , + ctx + ); + + expect(screen.getByText("Protected Content")).toBeInTheDocument(); + }); + + it("shows a loading spinner while auth state is being resolved", () => { + const ctx = buildAuthContext({ isLoading: true }); + + renderWithAuth( + + + , + ctx + ); + + expect(screen.queryByText("Protected Content")).not.toBeInTheDocument(); + // The spinner is rendered as an animate-spin div + expect(document.querySelector(".animate-spin")).toBeInTheDocument(); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 3. EmailVerify Component +// ═════════════════════════════════════════════════════════════════════════════ +import EmailVerify from "../src/pages/userauth/EmailVerify"; + +describe("EmailVerify Component", () => { + let ctx; + + beforeEach(() => { + vi.clearAllMocks(); + ctx = buildAuthContext(); + Storage.prototype.setItem = vi.fn(); + }); + + it("renders 6 OTP input boxes and a submit button", () => { + renderWithAuth(, ctx, { route: "/verify-account" }); + + const inputs = screen.getAllByRole("textbox"); + expect(inputs).toHaveLength(6); + expect(screen.getByRole("button", { name: /submit/i })).toBeInTheDocument(); + }); + + it("moves focus to the next box after typing a digit", async () => { + renderWithAuth(, ctx, { route: "/verify-account" }); + + const inputs = screen.getAllByRole("textbox"); + fireEvent.input(inputs[0], { target: { value: "1" } }); + + // After input, the next box should receive focus + // (jsdom doesn't natively track focus perfectly, but we verify the handler was wired) + expect(inputs[0]).toBeInTheDocument(); + }); + + it("submits OTP and navigates to /gamer-profile on success", async () => { + axios.post.mockResolvedValueOnce({ + data: { success: true, accessToken: "otp-token", message: "Email verified successfully!" }, + }); + ctx.getUserData.mockResolvedValueOnce(); + + renderWithAuth(, ctx, { + route: "/verify-account", + }); + + const inputs = screen.getAllByRole("textbox"); + const digits = ["1", "2", "3", "4", "5", "6"]; + digits.forEach((d, i) => { + fireEvent.change(inputs[i], { target: { value: d } }); + }); + + fireEvent.click(screen.getByRole("button", { name: /submit/i })); + + await waitFor(() => { + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/verify-account"), + expect.objectContaining({ otp: "123456" }) + ); + expect(ctx.setIsLoggedIn).toHaveBeenCalledWith(true); + expect(mockNavigate).toHaveBeenCalledWith("/gamer-profile"); + }); + }); + + it("shows error toast when OTP verification fails", async () => { + axios.post.mockResolvedValueOnce({ + data: { success: false, message: "Invalid OTP" }, + }); + + const { toast } = await import("react-toastify"); + renderWithAuth(, ctx, { route: "/verify-account" }); + + const inputs = screen.getAllByRole("textbox"); + inputs.forEach((input) => fireEvent.change(input, { target: { value: "0" } })); + + fireEvent.click(screen.getByRole("button", { name: /submit/i })); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith("Invalid OTP"); + }); + }); +}); + +// ═════════════════════════════════════════════════════════════════════════════ +// 4. AuthenticationContext +// ═════════════════════════════════════════════════════════════════════════════ +import { AppContextProvider } from "../src/context/userauth/authenticationContext"; +import { renderHook, act } from "@testing-library/react"; +import { useContext } from "react"; + +describe("AuthenticationContext", () => { + beforeEach(() => { + vi.clearAllMocks(); + Storage.prototype.getItem = vi.fn(() => null); + Storage.prototype.setItem = vi.fn(); + delete axios.defaults.headers.common["Authorization"]; + }); + + const wrapper = ({ children }) => ( + + {children} + + ); + + it("provides isLoggedIn as false by default", () => { + // Prevent the useEffect auth check from making real network calls + axios.get.mockRejectedValue(new Error("Network Error")); + + const { result } = renderHook(() => useContext(AppContent), { wrapper }); + expect(result.current.isLoggedIn).toBe(false); + }); + + it("sets isLoggedIn to true and calls getUserData when getAuthState succeeds", async () => { + axios.get + .mockResolvedValueOnce({ data: { success: true } }) // is-auth + .mockResolvedValueOnce({ data: { success: true, data: { name: "Alice" } } }); // user/data + + const { result } = renderHook(() => useContext(AppContent), { wrapper }); + + await waitFor(() => { + expect(result.current.isLoggedIn).toBe(true); + expect(result.current.userData).toMatchObject({ name: "Alice" }); + }); + }); + + it("keeps isLoggedIn false when auth check fails (backend down)", async () => { + axios.get.mockRejectedValue(new Error("Network Error")); + + const { result } = renderHook(() => useContext(AppContent), { wrapper }); + + await waitFor(() => { + expect(result.current.isLoggedIn).toBe(false); + expect(result.current.userData).toBe(false); + }); + }); + + it("setAuthHeader sets Authorization header when token exists in localStorage", () => { + Storage.prototype.getItem = vi.fn(() => "stored-token"); + axios.get.mockRejectedValue(new Error("Network Error")); + + const { result } = renderHook(() => useContext(AppContent), { wrapper }); + + act(() => { + result.current.setAuthHeader(); + }); + + expect(axios.defaults.headers.common["Authorization"]).toBe("Bearer stored-token"); + }); + + it("setAuthHeader removes Authorization header when no token in localStorage", () => { + Storage.prototype.getItem = vi.fn(() => null); + axios.defaults.headers.common["Authorization"] = "Bearer old-token"; + axios.get.mockRejectedValue(new Error("Network Error")); + + const { result } = renderHook(() => useContext(AppContent), { wrapper }); + + act(() => { + result.current.setAuthHeader(); + }); + + expect(axios.defaults.headers.common["Authorization"]).toBeUndefined(); + }); +}); diff --git a/crackcode/client/tests/CareerMap.test.jsx b/crackcode/client/tests/CareerMap.test.jsx new file mode 100644 index 00000000..6cb801ef --- /dev/null +++ b/crackcode/client/tests/CareerMap.test.jsx @@ -0,0 +1,445 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { MemoryRouter, Routes, Route } from "react-router-dom"; +import { vi } from "vitest"; + +// ── Hoisted mocks ───────────────────────────────────────────────────────────── +const { mockNavigate } = vi.hoisted(() => ({ mockNavigate: vi.fn() })); + +// ── Module mocks ────────────────────────────────────────────────────────────── +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); + return { ...actual, useNavigate: () => mockNavigate }; +}); + +vi.mock("../src/services/api/careermapService", () => ({ + fetchProgress: vi.fn(), + fetchChapterQuestionCount: vi.fn(), + fetchChapterQuestions: vi.fn(), + updateProgress: vi.fn(), + toBackendCareerId: vi.fn((id) => + ({ "Software-Engineer": "SoftwareEngineer", "ML-Engineer": "MLEngineer", "Data-Scientist": "DataScientist" }[id] || id) + ), +})); + +vi.mock("../src/pages/careermap/CareerChapters", () => ({ + getChapterByCareerId: vi.fn(), +})); + +// Stub layout/nav components that need auth or theme context +vi.mock("../src/components/common/Footer", () => ({ default: () =>
})); +vi.mock("../src/components/common/HQBtn", () => ({ default: () => })); +vi.mock("../src/components/common/Header", () => ({ default: () =>
})); + +// ── Component / data imports ────────────────────────────────────────────────── +import CareermapMain from "../src/pages/careermap/CareermapMain"; +import CareerChapterSelectionPage from "../src/pages/careermap/CareerChapterSelection"; +import ResultsPage from "../src/pages/careermap/Results"; +import { + careers, + getCareerById, + getCareersByDifficulty, + getCareersByCategory, + getUnlockedCareers, + getLockedCareers, + getDifficultyLabel, +} from "../src/pages/careermap/careers"; +import { fetchProgress, fetchChapterQuestionCount } from "../src/services/api/careermapService"; +import { getChapterByCareerId } from "../src/pages/careermap/CareerChapters"; + +// ── Shared test data ────────────────────────────────────────────────────────── +const mockChapters = [ + { + id: "oop", + title: "Object Oriented Programming", + description: "Core OOP concepts", + icon: "oop-icon", + categories: ["General Programming"], + }, + { + id: "dsa", + title: "Data Structures & Algorithms", + description: "DSA essentials", + icon: "dsa-icon", + categories: ["Data Structures"], + }, +]; + +// ── Render helpers ──────────────────────────────────────────────────────────── +const renderInRouter = (ui, route = "/") => + render({ui}); + +/** Renders a component inside Routes so useParams() is populated. */ +const renderChapterPage = (careerId = "Software-Engineer", state = null) => + render( + + + } /> + + + ); + +// ============================================================================= +// 1. careers.jsx — Pure utility functions +// ============================================================================= +describe("careers.jsx – Utility Functions", () => { + test("exports exactly 6 career entries", () => { + expect(careers).toHaveLength(6); + }); + + test("every career has required fields", () => { + careers.forEach((c) => { + expect(c).toHaveProperty("id"); + expect(c).toHaveProperty("title"); + expect(c).toHaveProperty("difficulty"); + expect(typeof c.locked).toBe("boolean"); + expect(Array.isArray(c.focus)).toBe(true); + }); + }); + + test("getCareerById returns the matching career", () => { + const se = getCareerById("Software-Engineer"); + expect(se).toBeDefined(); + expect(se.title).toBe("Software Engineer"); + }); + + test("getCareerById returns undefined for unknown id", () => { + expect(getCareerById("nonexistent-id")).toBeUndefined(); + }); + + test("getCareersByDifficulty returns only careers of that difficulty", () => { + const easy = getCareersByDifficulty("easy"); + expect(easy.length).toBeGreaterThan(0); + expect(easy.every((c) => c.difficulty === "easy")).toBe(true); + + const hard = getCareersByDifficulty("hard"); + expect(hard.every((c) => c.difficulty === "hard")).toBe(true); + }); + + test("getCareersByCategory returns only careers of that category", () => { + const software = getCareersByCategory("software"); + expect(software.length).toBeGreaterThan(0); + expect(software.every((c) => c.category === "software")).toBe(true); + }); + + test("getUnlockedCareers returns only unlocked careers", () => { + const unlocked = getUnlockedCareers(); + expect(unlocked.length).toBeGreaterThan(0); + expect(unlocked.every((c) => !c.locked)).toBe(true); + }); + + test("getLockedCareers returns only locked careers", () => { + const locked = getLockedCareers(); + expect(locked.length).toBeGreaterThan(0); + expect(locked.every((c) => c.locked)).toBe(true); + }); + + test("unlocked + locked counts equal total careers", () => { + expect(getUnlockedCareers().length + getLockedCareers().length).toBe(careers.length); + }); + + test.each([ + ["easy", "BEGINNER"], + ["medium", "INTERMEDIATE"], + ["hard", "ADVANCED"], + ["unknown", "BEGINNER"], // fallback + ])("getDifficultyLabel('%s') returns '%s'", (input, expected) => { + expect(getDifficultyLabel(input)).toBe(expected); + }); +}); + +// ============================================================================= +// 2. CareermapMain — Career Selection Page +// ============================================================================= +describe("CareermapMain – Career Selection Page", () => { + beforeEach(() => mockNavigate.mockClear()); + + test("renders the page heading", () => { + renderInRouter(); + expect(screen.getByText(/Choose the best career path/i)).toBeInTheDocument(); + }); + + test("renders all 6 career titles", () => { + renderInRouter(); + [ + "Software Engineer", + "ML Engineer", + "Data Scientist", + "Backend Developer", + "Game Developer", + "Web Developer", + ].forEach((title) => expect(screen.getByText(title)).toBeInTheDocument()); + }); + + test("renders difficulty badges (BEGINNER, INTERMEDIATE, ADVANCED)", () => { + renderInRouter(); + expect(screen.getByText("BEGINNER")).toBeInTheDocument(); + expect(screen.getAllByText("INTERMEDIATE").length).toBeGreaterThan(0); + expect(screen.getAllByText("ADVANCED").length).toBeGreaterThan(0); + }); + + test("navigates to career page when an unlocked card is clicked", () => { + renderInRouter(); + fireEvent.click(screen.getByText("Software Engineer")); + expect(mockNavigate).toHaveBeenCalledWith( + "/careermap/Software-Engineer", + expect.objectContaining({ state: expect.any(Object) }) + ); + }); + + test("does NOT navigate when a locked career card is clicked", () => { + renderInRouter(); + fireEvent.click(screen.getByText("Backend Developer")); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + test("locked careers (Backend Developer, Game Developer, Web Developer) are all rendered", () => { + renderInRouter(); + expect(screen.getByText("Backend Developer")).toBeInTheDocument(); + expect(screen.getByText("Game Developer")).toBeInTheDocument(); + expect(screen.getByText("Web Developer")).toBeInTheDocument(); + }); + + test("renders language info for Software Engineer card", () => { + renderInRouter(); + expect(screen.getByText(/Python\/Java/i)).toBeInTheDocument(); + }); +}); + +// ============================================================================= +// 3. CareerChapterSelectionPage — Chapter Selection +// ============================================================================= +describe("CareerChapterSelectionPage – Chapter Selection", () => { + beforeEach(() => { + mockNavigate.mockClear(); + vi.clearAllMocks(); + getChapterByCareerId.mockReturnValue(mockChapters); + fetchProgress.mockResolvedValue({ chapters: [] }); + fetchChapterQuestionCount.mockResolvedValue(15); + Storage.prototype.getItem = vi.fn(() => null); + }); + + test("renders career title from navigation state", async () => { + renderChapterPage("Software-Engineer", { title: "Software Engineer" }); + expect(await screen.findByText("Software Engineer")).toBeInTheDocument(); + }); + + test("falls back to CAREER_TITLES map when no navigation state provided", async () => { + renderChapterPage("Software-Engineer", null); + expect(await screen.findByText("Software Engineer")).toBeInTheDocument(); + }); + + test("renders all chapter titles", async () => { + renderChapterPage(); + expect(await screen.findByText("Object Oriented Programming")).toBeInTheDocument(); + expect(await screen.findByText("Data Structures & Algorithms")).toBeInTheDocument(); + }); + + test("first chapter is always unlocked (Start Quiz button visible)", async () => { + renderChapterPage(); + const startBtns = await screen.findAllByText(/Start Quiz/i); + expect(startBtns.length).toBeGreaterThanOrEqual(1); + }); + + test("second chapter is locked when first has not been passed", async () => { + // fetchProgress returns no passed chapters; localStorage returns null + renderChapterPage(); + await waitFor(() => screen.getByText("Data Structures & Algorithms")); + expect(screen.getAllByText(/Start Quiz/i)).toHaveLength(1); + }); + + test("second chapter unlocks when first chapter is marked as passed", async () => { + fetchProgress.mockResolvedValue({ + chapters: [{ chapterId: "oop", passed: true, easyScore: 5, mediumScore: 5, hardScore: 5 }], + }); + renderChapterPage(); + await waitFor(() => screen.getByText("Data Structures & Algorithms")); + expect(screen.getAllByText(/Start Quiz/i)).toHaveLength(2); + }); + + test("calls fetchProgress with the correct careerId", async () => { + renderChapterPage("Software-Engineer"); + await waitFor(() => expect(fetchProgress).toHaveBeenCalledWith("Software-Engineer")); + }); + + test("calls fetchChapterQuestionCount for each chapter", async () => { + renderChapterPage(); + await waitFor(() => + expect(fetchChapterQuestionCount).toHaveBeenCalledTimes(mockChapters.length) + ); + }); + + test("shows 'Career path not found' for an unknown careerId", async () => { + getChapterByCareerId.mockReturnValue([]); + renderChapterPage("unknown-career"); + expect(await screen.findByText(/Career path not found/i)).toBeInTheDocument(); + }); + + test("'Back' link navigates to /careermap when career is not found", async () => { + getChapterByCareerId.mockReturnValue([]); + renderChapterPage("unknown-career"); + await screen.findByText(/Career path not found/i); + fireEvent.click(screen.getByText(/← Back/i)); + expect(mockNavigate).toHaveBeenCalledWith("/careermap"); + }); + + test("navigates to quiz page when an unlocked chapter is clicked", async () => { + renderChapterPage("Software-Engineer", { title: "Software Engineer" }); + const startBtns = await screen.findAllByText(/Start Quiz/i); + fireEvent.click(startBtns[0]); + expect(mockNavigate).toHaveBeenCalledWith( + "/careermap/Software-Engineer/quiz/oop", + expect.objectContaining({ state: expect.any(Object) }) + ); + }); + + test("falls back to localStorage when fetchProgress rejects", async () => { + fetchProgress.mockRejectedValue(new Error("Network error")); + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_oop_passed" ? "true" : null + ); + renderChapterPage("Software-Engineer"); + // oop passed via localStorage → dsa should be unlocked + await waitFor(() => + expect(screen.getAllByText(/Start Quiz/i)).toHaveLength(2) + ); + }); +}); + +// ============================================================================= +// 4. ResultsPage — Quiz Results +// ============================================================================= +describe("ResultsPage – Quiz Results", () => { + const defaultProps = { + score: 10, + total: 15, + title: "Software Engineer", + subtitle: "Object Oriented Programming", + careerId: "Software-Engineer", + currentChapterId: "oop", + onRestart: vi.fn(), + }; + + beforeEach(() => { + mockNavigate.mockClear(); + Storage.prototype.getItem = vi.fn(() => null); + getChapterByCareerId.mockReturnValue(mockChapters); + }); + + const renderResults = (props = {}) => + renderInRouter(); + + test("renders the career title and chapter subtitle", () => { + renderResults(); + // Title may appear in both the heading and the completion banner; check at least one exists + expect(screen.getAllByText("Software Engineer").length).toBeGreaterThan(0); + expect(screen.getByText("Object Oriented Programming")).toBeInTheDocument(); + }); + + test("renders the score in the score circle", () => { + renderResults({ score: 10 }); + // The score circle contains the raw number + expect(screen.getAllByText("10").length).toBeGreaterThan(0); + }); + + test("renders correct/wrong/percentage stats", () => { + renderResults({ score: 10, total: 15 }); + expect(screen.getByText("5")).toBeInTheDocument(); // wrong = 15 - 10 + expect(screen.getByText("67%")).toBeInTheDocument(); // Math.round(10/15*100) + expect(screen.getByText("Correct")).toBeInTheDocument(); + expect(screen.getByText("Wrong")).toBeInTheDocument(); + expect(screen.getByText("Score")).toBeInTheDocument(); + }); + + test("shows 'Perfect Score' when score equals total", () => { + renderResults({ score: 15, total: 15 }); + expect(screen.getByText(/Perfect Score/i)).toBeInTheDocument(); + }); + + test("shows 'Well Done' when score is at least half of total", () => { + renderResults({ score: 10, total: 15 }); + expect(screen.getByText(/Well Done/i)).toBeInTheDocument(); + }); + + test("shows 'Keep Practicing' when score is below half of total", () => { + renderResults({ score: 5, total: 15 }); + expect(screen.getByText(/Keep Practicing/i)).toBeInTheDocument(); + }); + + test("always renders a 'Try Again' button", () => { + renderResults(); + expect(screen.getByText("Try Again")).toBeInTheDocument(); + }); + + test("calls onRestart when 'Try Again' is clicked", () => { + const onRestart = vi.fn(); + renderResults({ onRestart }); + fireEvent.click(screen.getByText("Try Again")); + expect(onRestart).toHaveBeenCalled(); + }); + + test("shows 'Back to Chapters' when score < 8", () => { + renderResults({ score: 5 }); + expect(screen.getByText(/Back to Chapters/i)).toBeInTheDocument(); + }); + + test("navigates to chapter selection on 'Back to Chapters' click", () => { + renderResults({ score: 5 }); + fireEvent.click(screen.getByText(/Back to Chapters/i)); + expect(mockNavigate).toHaveBeenCalledWith("/careermap/Software-Engineer"); + }); + + test("shows 'Chapter unlocked!' banner when chapter is passed and a next chapter exists", async () => { + // Mark oop as passed in localStorage; mockChapters has dsa as next + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_oop_passed" ? "true" : null + ); + renderResults({ score: 10 }); + expect(await screen.findByText("Chapter unlocked!")).toBeInTheDocument(); + }); + + test("shows 'Next' button when chapter is passed and a next chapter exists", async () => { + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_oop_passed" ? "true" : null + ); + renderResults({ score: 10 }); + expect(await screen.findByText("Next")).toBeInTheDocument(); + }); + + test("navigates to next chapter quiz on 'Next' button click", async () => { + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_oop_passed" ? "true" : null + ); + renderResults({ score: 10 }); + fireEvent.click(await screen.findByText("Next")); + expect(mockNavigate).toHaveBeenCalledWith( + "/careermap/Software-Engineer/quiz/dsa", + expect.objectContaining({ state: expect.any(Object) }) + ); + }); + + test("shows 'Career Path Completed!' on the last chapter when passed", async () => { + // Use dsa (index 1 = last in mockChapters) as current chapter + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_dsa_passed" ? "true" : null + ); + renderResults({ score: 10, currentChapterId: "dsa" }); + expect(await screen.findByText(/Career Path Completed/i)).toBeInTheDocument(); + }); + + test("shows 'Back to Career Map' on the last chapter when passed", async () => { + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_dsa_passed" ? "true" : null + ); + renderResults({ score: 10, currentChapterId: "dsa" }); + expect(await screen.findByText("Back to Career Map")).toBeInTheDocument(); + }); + + test("navigates to /careermap on 'Back to Career Map' click", async () => { + Storage.prototype.getItem = vi.fn((key) => + key === "Software-Engineer_dsa_passed" ? "true" : null + ); + renderResults({ score: 10, currentChapterId: "dsa" }); + fireEvent.click(await screen.findByText("Back to Career Map")); + expect(mockNavigate).toHaveBeenCalledWith("/careermap"); + }); +}); diff --git a/crackcode/client/tests/CodeEditor.test.jsx b/crackcode/client/tests/CodeEditor.test.jsx new file mode 100644 index 00000000..c5761ae3 --- /dev/null +++ b/crackcode/client/tests/CodeEditor.test.jsx @@ -0,0 +1,629 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; + +// ─── Shared mocks ───────────────────────────────────────────────────────────── + +const { mockNavigate } = vi.hoisted(() => ({ mockNavigate: vi.fn() })); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockNavigate, + useParams: () => ({ problemId: 'py_fundamentals_001' }), + useLocation: () => ({ state: null, pathname: '/editor/py_fundamentals_001' }), + }; +}); + +// Mock @monaco-editor/react — jsdom cannot run Monaco +vi.mock('@monaco-editor/react', () => ({ + default: ({ onChange, value, language }) => ( +