diff --git a/client/cucumber.js b/client/cucumber.js index d4ecbe35..ef8250b2 100644 --- a/client/cucumber.js +++ b/client/cucumber.js @@ -1,3 +1,5 @@ +process.env.TS_NODE_PROJECT = 'tsconfig.test.json'; + module.exports = { default: { require: [ diff --git a/client/package-lock.json b/client/package-lock.json index 7089679f..d2d6b4c8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,11 +13,13 @@ "react-scripts": "5.0.1" }, "devDependencies": { - "@cucumber/cucumber": "^12.2.0", + "@cucumber/cucumber": "^12.3.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/cucumber": "^6.0.1", "@types/jest": "^30.0.0", + "@types/node": "^24.10.1", "@types/puppeteer": "^5.4.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", @@ -2416,31 +2418,30 @@ } }, "node_modules/@cucumber/ci-environment": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", - "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-12.0.0.tgz", + "integrity": "sha512-SqCEnbCNl3zCXCFpqGUuoaSNhLC0jLw4tKeFcAxTw9MD/QRlJjeAC/fyvVLFuXuSq0OunJlFfxLu+Z3HE+oLPg==", "dev": true, "license": "MIT" }, "node_modules/@cucumber/cucumber": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.2.0.tgz", - "integrity": "sha512-b7W4snvXYi1T2puUjxamASCCNhNzVSzb/fQUuGSkdjm/AFfJ24jo8kOHQyOcaoArCG71sVQci4vkZaITzl/V1w==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.3.0.tgz", + "integrity": "sha512-36cIyplE1iDl12s4k6lBVpceua8tKLklFTf7CUITPrNHTLlQ/KBr7NYUUHviPzCbj2Ox3BPTZ6qkSLd6WMvVQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@cucumber/ci-environment": "10.0.1", + "@cucumber/ci-environment": "12.0.0", "@cucumber/cucumber-expressions": "18.0.1", - "@cucumber/gherkin": "34.0.0", - "@cucumber/gherkin-streams": "5.0.1", - "@cucumber/gherkin-utils": "9.2.0", - "@cucumber/html-formatter": "21.14.0", - "@cucumber/junit-xml-formatter": "0.8.1", + "@cucumber/gherkin": "37.0.0", + "@cucumber/gherkin-streams": "6.0.0", + "@cucumber/gherkin-utils": "10.0.0", + "@cucumber/html-formatter": "22.2.0", + "@cucumber/junit-xml-formatter": "0.9.0", "@cucumber/message-streams": "4.0.1", - "@cucumber/messages": "28.1.0", + "@cucumber/messages": "31.0.0", "@cucumber/pretty-formatter": "1.0.1", - "@cucumber/tag-expressions": "6.2.0", + "@cucumber/tag-expressions": "8.1.0", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", "chalk": "^4.1.2", @@ -2449,7 +2450,7 @@ "debug": "^4.3.4", "error-stack-parser": "^2.1.4", "figures": "^3.2.0", - "glob": "^11.0.0", + "glob": "^13.0.0", "has-ansi": "^4.0.1", "indent-string": "^4.0.0", "is-installed-globally": "^0.4.0", @@ -2457,19 +2458,19 @@ "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", - "luxon": "3.7.1", + "luxon": "3.7.2", "mime": "^3.0.0", "mkdirp": "^3.0.0", "mz": "^2.7.0", "progress": "^2.0.3", - "read-package-up": "^11.0.0", - "semver": "7.7.2", + "read-package-up": "^12.0.0", + "semver": "7.7.3", "string-argv": "0.3.1", "supports-color": "^8.1.1", "type-fest": "^4.41.0", "util-arity": "^1.1.0", "yaml": "^2.2.2", - "yup": "1.7.0" + "yup": "1.7.1" }, "bin": { "cucumber-js": "bin/cucumber.js" @@ -2491,6 +2492,17 @@ "regexp-match-indices": "1.0.2" } }, + "node_modules/@cucumber/cucumber/node_modules/@cucumber/messages": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", + "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" + } + }, "node_modules/@cucumber/cucumber/node_modules/commander": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", @@ -2502,38 +2514,16 @@ } }, "node_modules/@cucumber/cucumber/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@cucumber/cucumber/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, "engines": { "node": "20 || >=22" }, @@ -2542,11 +2532,11 @@ } }, "node_modules/@cucumber/cucumber/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -2613,19 +2603,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@cucumber/cucumber/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@cucumber/cucumber/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2669,24 +2646,23 @@ } }, "node_modules/@cucumber/gherkin": { - "version": "34.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", - "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-37.0.0.tgz", + "integrity": "sha512-vKJVJ6h4HCktG870wgYUUskNpFxbFI0WmAkVLPTz1LlLwJX7/KOBqFcr2/L3u0pPoHjbLRW+IpbiXLT2T13/wg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@cucumber/messages": ">=19.1.4 <29" + "@cucumber/messages": ">=31.0.0 <32" } }, "node_modules/@cucumber/gherkin-streams": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", - "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-6.0.0.tgz", + "integrity": "sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==", "dev": true, "license": "MIT", "dependencies": { - "commander": "9.1.0", + "commander": "14.0.0", "source-map-support": "0.5.21" }, "bin": { @@ -2699,26 +2675,26 @@ } }, "node_modules/@cucumber/gherkin-streams/node_modules/commander": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", - "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=20" } }, "node_modules/@cucumber/gherkin-utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", - "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-10.0.0.tgz", + "integrity": "sha512-BcujlDT343GXXNrMPl3ws6Il3zs8dQw3Yp/d3HnOJF8i2snGGgiapoTbko7MdvAt7ivDL7SDo+e1d5Cnpl3llA==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/gherkin": "^31.0.0", - "@cucumber/messages": "^27.0.0", + "@cucumber/gherkin": "^34.0.0", + "@cucumber/messages": "^29.0.0", "@teppeis/multimaps": "3.0.0", - "commander": "13.1.0", + "commander": "14.0.0", "source-map-support": "^0.5.21" }, "bin": { @@ -2726,69 +2702,53 @@ } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", - "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", + "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <=26" + "@cucumber/messages": ">=19.1.4 <29" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", - "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-28.1.0.tgz", + "integrity": "sha512-2LzZtOwYKNlCuNf31ajkrekoy2M4z0Z1QGiPH40n4gf5t8VOUFb7m1ojtR4LmGvZxBGvJZP8voOmRqDWzBzYKA==", "dev": true, "license": "MIT", "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", - "uuid": "10.0.0" - } - }, - "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "uuid": "11.1.0" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", - "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-29.0.1.tgz", + "integrity": "sha512-aAvIYfQD6/aBdF8KFQChC3CQ1Q+GX9orlR6GurGiX6oqaCnBkxA4WU3OQUVepDynEFrPayerqKRFcAMhdcXReQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/uuid": "10.0.0", "class-transformer": "0.5.1", - "reflect-metadata": "0.2.2", - "uuid": "11.0.5" + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/gherkin-utils/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@cucumber/gherkin-utils/node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", @@ -2799,10 +2759,21 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", + "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" + } + }, "node_modules/@cucumber/html-formatter": { - "version": "21.14.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.14.0.tgz", - "integrity": "sha512-vQqbmQZc0QiN4c+cMCffCItpODJlOlYtPG7pH6We096dBOa7u0ttDMjT6KrMAnQlcln54rHL46r408IFpuznAw==", + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-22.2.0.tgz", + "integrity": "sha512-fUNC/KngTIz+hAQ2Yr4XjdYq+MO60PwK9SidxBQ54jNI1Vw7erlgsPq0TOWneCIvdjU3qp+YDqYG1hw3zuUuDA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2810,13 +2781,13 @@ } }, "node_modules/@cucumber/junit-xml-formatter": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.8.1.tgz", - "integrity": "sha512-FT1Y96pyd9/ifbE9I7dbkTCjkwEdW9C0MBobUZoKD13c8EnWAt0xl1Yy/v/WZLTk4XfCLte1DATtLx01jt+YiA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.9.0.tgz", + "integrity": "sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==", "dev": true, "license": "MIT", "dependencies": { - "@cucumber/query": "^13.0.2", + "@cucumber/query": "^14.0.1", "@teppeis/multimaps": "^3.0.0", "luxon": "^3.5.0", "xmlbuilder": "^15.1.1" @@ -2895,9 +2866,9 @@ } }, "node_modules/@cucumber/query": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", - "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-14.6.0.tgz", + "integrity": "sha512-bPbfpkDsFCBn95erh3un76QViPqGAo3T7iYews0yA3/JRNoV009s7acxxY+f+OMABPFl0TJVIZlvqX+KayQ+Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -2909,9 +2880,9 @@ } }, "node_modules/@cucumber/tag-expressions": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.2.0.tgz", - "integrity": "sha512-KIF0eLcafHbWOuSDWFw0lMmgJOLdDRWjEL1kfXEWrqHmx2119HxVAr35WuEd9z542d3Yyg+XNqSr+81rIKqEdg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-8.1.0.tgz", + "integrity": "sha512-UFeOVUyc711/E7VHjThxMwg3jbGod9TlbM1gxNixX/AGDKg82Eha4cE0tKki3GGUs7uB2NyI+hQAuhB8rL2h5A==", "dev": true, "license": "MIT" }, @@ -4088,6 +4059,13 @@ "@types/node": "*" } }, + "node_modules/@types/cucumber": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cucumber/-/cucumber-6.0.1.tgz", + "integrity": "sha512-+GZV6xfN0MeN9shDCdny8GbC8N0+U6uca8cjyaJndcwmrUhwS6qOU2vmYn0d71EOwJF568/v3SxJ8VKxuZNYRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -4416,9 +4394,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10022,24 +10000,27 @@ } }, "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "dev": true, "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/hpack.js": { "version": "2.1.6", @@ -12395,9 +12376,9 @@ } }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "dev": true, "license": "MIT", "engines": { @@ -12789,18 +12770,18 @@ "license": "MIT" }, "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "hosted-git-info": "^7.0.0", + "hosted-git-info": "^9.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -16383,51 +16364,54 @@ } }, "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", "dev": true, "license": "MIT", "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.0.0.tgz", + "integrity": "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==", "dev": true, "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.2.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -16451,7 +16435,7 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg/node_modules/type-fest": { + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", @@ -16464,6 +16448,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -18458,6 +18458,19 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwindcss": { "version": "3.4.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", @@ -19228,9 +19241,9 @@ } }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -20381,9 +20394,9 @@ } }, "node_modules/yup": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.0.tgz", - "integrity": "sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/client/package.json b/client/package.json index c6d61325..2395f152 100644 --- a/client/package.json +++ b/client/package.json @@ -8,11 +8,13 @@ "react-scripts": "5.0.1" }, "devDependencies": { - "@cucumber/cucumber": "^12.2.0", + "@cucumber/cucumber": "^12.3.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/cucumber": "^6.0.1", "@types/jest": "^30.0.0", + "@types/node": "^24.10.1", "@types/puppeteer": "^5.4.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", diff --git a/client/src/App.css b/client/src/App.css index 8cabd26f..6745f602 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -59,6 +59,18 @@ body { gap: 0.5rem; } +.success-message { + background-color: #f0fff4; + color: #065f46; + border: 1px solid #34d399; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + /* Student Form */ .student-form { background: white; @@ -771,6 +783,76 @@ tbody tr:last-child td { background-color: #2563eb; } +/* Metas action button in classes list (yellow, same scale as other action buttons) */ +.metas-btn { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + color: white; + padding: 0.5rem 1rem; + font-size: 0.8rem; + min-width: auto; + border-radius: 4px; +} + +.metas-btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(245, 158, 11, 0.25); +} + +/* Slight vertical offset so Metas button doesn't sit flush with Edit */ +.metas-btn { position: relative; top: 2px; } + +/* Metas modal buttons */ +.meta-add-btn { + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); /* green */ + color: white; + padding: 0.5rem 1rem; + font-size: 0.8rem; +} +.meta-add-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(72,187,120,0.2); } + +.meta-edit-btn { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); /* yellow */ + color: white; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + border-radius: 6px; + margin-left: 0.25rem; +} +.meta-edit-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(245,158,11,0.2); } + +.meta-delete-btn { + background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); /* red */ + color: white; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + border-radius: 6px; + margin-left: 0.25rem; +} +.meta-delete-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(245,101,101,0.2); } + +/* Create metas: same visual as primary submit button */ +.meta-create-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: rgb(255, 255, 255); + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-weight: 600; +} + +/* Make metas modal input a bit larger */ +.metas-modal-content .add-meta-row input { + width: 60%; + padding: 1rem; + font-size: 1rem; + border: 2px solid #d1d5db; + border-radius: 6px; +} + +.metas-modal-content .add-meta-row button { min-width: 80px; } + +.local-metas-list { list-style: none; margin-top: 1rem; padding-left: 0; } +.local-meta-item { display:flex; justify-content:space-between; align-items:center; gap: 1rem; padding: 0.5rem 0; border-bottom: 1px solid #f1f5f9; } + /* Evaluations Table */ .evaluations-table { width: 100%; @@ -1013,7 +1095,7 @@ tbody tr:last-child td { border-collapse: collapse; background: white; min-width: 800px; - table-layout: fixed; /* Fixed layout for uniform column widths */ + table-layout: auto; /* allow columns to size based on content so long metas can expand */ } .student-name-header { @@ -1040,11 +1122,14 @@ tbody tr:last-child td { font-weight: 700; font-size: 0.9rem; border: 1px solid #047857; - width: calc((100% - 200px) / 6); /* Equal width for all 6 goal columns */ + width: auto; /* allow width to expand based on content */ + min-width: 160px; /* reasonable minimum for goals */ text-shadow: 0 1px 2px rgba(0,0,0,0.3); box-shadow: inset 0 1px 0 rgba(255,255,255,0.2); vertical-align: middle; line-height: 1.2; + white-space: normal; + word-wrap: break-word; } .student-row:nth-child(even) { @@ -1089,10 +1174,17 @@ tbody tr:last-child td { padding: 12px 8px; text-align: center; border: 1px solid #cbd5e1; - width: calc((100% - 200px) / 6); /* Equal width for all goal columns */ + width: auto; + min-width: 140px; vertical-align: middle; } +/* Center selects inside evaluation cells */ +.evaluation-cell select { margin: 0 auto; display: block; } + +/* Metas modal actions spacing */ +.metas-actions { display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1rem; } + .student-row:nth-child(even) .evaluation-cell { background-color: #f0f9ff; } diff --git a/client/src/App.tsx b/client/src/App.tsx index cc987904..f8af5f6c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -17,6 +17,7 @@ const App: React.FC = () => { const [selectedClass, setSelectedClass] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); const [editingStudent, setEditingStudent] = useState(null); const [activeTab, setActiveTab] = useState('students'); @@ -109,6 +110,12 @@ const App: React.FC = () => { setError(errorMessage); }; + const handleSuccess = (message: string) => { + setSuccess(message); + // Clear success after a short timeout + setTimeout(() => setSuccess(''), 5000); + }; + return (
@@ -122,6 +129,11 @@ const App: React.FC = () => { Error: {error}
)} + {success && ( +
+ {success} +
+ )} {/* Tab Navigation */}
@@ -213,6 +225,7 @@ const App: React.FC = () => { onClassUpdated={handleClassUpdated} onClassDeleted={handleClassDeleted} onError={handleError} + onSuccess={handleSuccess} /> )}
diff --git a/client/src/components/Classes.tsx b/client/src/components/Classes.tsx index e5d4302f..ad8baaf2 100644 --- a/client/src/components/Classes.tsx +++ b/client/src/components/Classes.tsx @@ -12,6 +12,7 @@ interface ClassesProps { onClassUpdated: () => void; onClassDeleted: () => void; onError: (errorMessage: string) => void; + onSuccess?: (message: string) => void; } const Classes: React.FC = ({ @@ -19,7 +20,8 @@ const Classes: React.FC = ({ onClassAdded, onClassUpdated, onClassDeleted, - onError + onError, + onSuccess }) => { const [formData, setFormData] = useState({ topic: '', @@ -27,6 +29,12 @@ const Classes: React.FC = ({ year: new Date().getFullYear(), especificacaoDoCalculoDaMedia: DEFAULT_ESPECIFICACAO_DO_CALCULO_DE_MEDIA }); + // Metas management state (local, before sending to server) + const [localMetas, setLocalMetas] = useState([]); + const [editingMetaIndex, setEditingMetaIndex] = useState(null); + const [editingMetaValue, setEditingMetaValue] = useState(''); + // Separate modal state for metas management + const [metaPanelClass, setMetaPanelClass] = useState(null); const [editingClass, setEditingClass] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -151,7 +159,13 @@ const Classes: React.FC = ({ setEditingClass(null); } else { // Add new class - await ClassService.addClass(formData); + const created = await ClassService.addClass(formData); + // After creating a class, prepare localMetas for metas flow + if (created && created.id) { + setLocalMetas([]); + // open enrollment panel automatically for the newly created class + setEnrollmentPanelClass(created); + } onClassAdded(); } @@ -169,6 +183,59 @@ const Classes: React.FC = ({ } }; + // Metas UI handlers + // Add meta locally; do not block duplicates here — server is responsible for uniqueness + const handleAddMetaLocally = (meta: string) => { + if (!meta || !meta.trim()) return; + setLocalMetas(prev => [...prev, meta]); + }; + + const handleEditMetaStart = (index: number) => { + setEditingMetaIndex(index); + setEditingMetaValue(localMetas[index]); + }; + + const handleEditMetaSave = () => { + if (editingMetaIndex === null) return; + const newVal = editingMetaValue.trim(); + if (!newVal) return; + setLocalMetas(prev => prev.map((m, i) => i === editingMetaIndex ? newVal : m)); + setEditingMetaIndex(null); + setEditingMetaValue(''); + }; + + const handleDeleteMeta = (index: number) => { + setLocalMetas(prev => prev.filter((_, i) => i !== index)); + }; + + // Post metas to server for a given class id (uses ClassService) + const handleCreateMetasOnServer = async (classObj: Class) => { + try { + if (!classObj || !classObj.id) { + onError('Classe inválida'); + return; + } + + const data = await ClassService.setMetas(classObj.id, localMetas); + + // Show server message if provided + const serverMessage = data && (data.message || data.msg) ? (data.message || data.msg) : 'Metas criadas com sucesso!'; + if (onSuccess) { + onSuccess(serverMessage); + } else { + onError(serverMessage); + } + + // Refresh classes list and close metas panel + await loadAllStudents(); + onClassUpdated(); + setEnrollmentPanelClass(null); + setMetaPanelClass(null); + } catch (error) { + onError((error as Error).message); + } + }; + // Handle edit button click const handleEdit = (classObj: Class) => { setEditingClass(classObj); @@ -323,6 +390,15 @@ const Classes: React.FC = ({ > Enroll + + + ))} @@ -342,8 +418,9 @@ const Classes: React.FC = ({ className="close-modal-btn" onClick={handleCloseEnrollmentPanel} title="Close" + aria-label="Close" > - × + × @@ -437,6 +514,89 @@ const Classes: React.FC = ({ )} + + {/* Metas Modal reusing enrollment modal design */} + {metaPanelClass && ( +
+
+
+

Metas for {metaPanelClass.topic}

+ +
+ +
+ {metaPanelClass.metas && metaPanelClass.metas.length > 0 ? ( +
+

Existing Metas

+
    + {metaPanelClass.metas.map((meta, idx) => ( +
  • + {meta} +
  • + ))} +
+
+ ) : ( + <> +
+ setEditingMetaValue(e.target.value)} + /> + + {editingMetaIndex !== null && ( + + )} +
+ +
    + {localMetas.length === 0 ? ( +
  • No local metas added yet
  • + ) : ( + localMetas.map((meta, idx) => ( +
  • + {meta} +
    + + +
    +
  • + )) + )} +
+ + )} + +
+ {metaPanelClass.metas && metaPanelClass.metas.length > 0 ? ( + + ) : ( + <> + + + + )} +
+
+
+
+ )} ); }; diff --git a/client/src/components/Evaluations.tsx b/client/src/components/Evaluations.tsx index 1b992a1d..e6e5bfec 100644 --- a/client/src/components/Evaluations.tsx +++ b/client/src/components/Evaluations.tsx @@ -18,15 +18,10 @@ const Evaluations: React.FC = ({ onError }) => { const [selectedClass, setSelectedClass] = useState(null); const [isLoading, setIsLoading] = useState(false); - // Predefined evaluation goals - const evaluationGoals = [ - 'Requirements', - 'Configuration Management', - 'Project Management', - 'Design', - 'Tests', - 'Refactoring' - ]; + // Use metas provided by the selected class; fallback to empty array + const evaluationGoals = selectedClass && Array.isArray(selectedClass.metas) && selectedClass.metas.length > 0 + ? selectedClass.metas + : []; const loadClasses = useCallback(async () => { try { @@ -127,7 +122,7 @@ const Evaluations: React.FC = ({ onError }) => { )} - {selectedClass && selectedClass.enrollments.length === 0 && ( + {selectedClass && selectedClass.enrollments.length === 0 && (
= ({ onError }) => {
)} - {selectedClass && selectedClass.enrollments.length > 0 && ( + {selectedClass && selectedClass.enrollments.length > 0 && (
{/*Componente de importacao de notas de uma planilha, vai reagir as mudacas do classId */}
@@ -155,9 +150,13 @@ const Evaluations: React.FC = ({ onError }) => { Student - {evaluationGoals.map(goal => ( - {goal} - ))} + {evaluationGoals.length > 0 ? ( + evaluationGoals.map(goal => ( + {goal} + )) + ) : ( + No metas defined + )} @@ -173,7 +172,8 @@ const Evaluations: React.FC = ({ onError }) => { return ( {student.name} - {evaluationGoals.map(goal => { + {evaluationGoals.length > 0 ? ( + evaluationGoals.map(goal => { const currentGrade = studentEvaluations[goal] || ''; return ( @@ -190,7 +190,10 @@ const Evaluations: React.FC = ({ onError }) => { ); - })} + }) + ) : ( + No metas + )} ); })} diff --git a/client/src/features/criacao-metas.feature b/client/src/features/criacao-metas.feature new file mode 100644 index 00000000..cf698f99 --- /dev/null +++ b/client/src/features/criacao-metas.feature @@ -0,0 +1,59 @@ +@gui +Feature: Criacao de metas + + Background: + Given a turma "engenharia-de-software-e-sistemas" existe no sistema + And estou na página de criação de metas da turma "engenharia-de-software-e-sistemas" + + Scenario: Criar metas com sucesso + Given não existe nenhuma meta cadastrada na turma "engenharia-de-software-e-sistemas" + When adiciono as metas "Requisitos" e "Testes de software" para a turma "engenharia-de-software-e-sistemas" + And eu submeto a criação de metas + Then vejo a notificação "Metas criadas com sucesso!" + And a listagem de metas da turma "engenharia-de-software-e-sistemas" exibe os itens com títulos "Requisitos" e "Testes de software" + + Scenario: Tentar criar metas sem título + When tento adicionar uma meta sem título para a turma "engenharia-de-software-e-sistemas" + Then eu vejo que não está disponível a opção de submissão de criação de metas + And a listagem de metas da turma "engenharia-de-software-e-sistemas" permanece vazia + + Scenario: Tentar criar metas duplicadas + Given não existe nenhuma meta cadastrada na turma "engenharia-de-software-e-sistemas" + When adiciono as metas "Requisitos" e "Requisitos" para a turma "engenharia-de-software-e-sistemas" + And eu submeto a criação de metas + Then vejo a notificação "Metas não podem conter duplicatas!" + And a listagem de metas da turma "engenharia-de-software-e-sistemas" permanece vazia + + Scenario: Cancelar a criação de metas para uma turma e criar para outra + Given não existe nenhuma meta cadastrada na turma "engenharia-de-software-e-sistemas" + And a turma "inteligencia-artificial" existe no sistema + And não existe nenhuma meta cadastrada na turma "inteligencia-artificial" + When adiciono as metas "Requisitos" e "Testes de software" para a turma "engenharia-de-software-e-sistemas" + And cancelo a criação de metas para a turma "engenharia-de-software-e-sistemas" + And adiciono as metas "Redes Neurais" e "Aprendizado de Máquina" para a turma "inteligencia-artificial" + And eu submeto a criação de metas + Then vejo a notificação "Metas criadas com sucesso!" + And a listagem de metas da turma "engenharia-de-software-e-sistemas" permanece vazia + And a listagem de metas da turma "inteligencia-artificial" exibe os itens com títulos "Redes Neurais" e "Aprendizado de Máquina" + + Scenario: Editar metas para serem cadastradas + Given não existe nenhuma meta cadastrada na turma "engenharia-de-software-e-sistemas" + When adiciono as metas "Requisitos" e "Testes de software" para a turma "engenharia-de-software-e-sistemas" + And edito a meta "Testes de software" para "Validação de Software" na turma "engenharia-de-software-e-sistemas" + And eu submeto a criação de metas + Then vejo a notificação "Metas criadas com sucesso!" + And a listagem de metas da turma "engenharia-de-software-e-sistemas" exibe os itens com títulos "Requisitos" e "Validação de Software" + + Scenario: Deletar metas para serem cadastradas + Given não existe nenhuma meta cadastrada na turma "engenharia-de-software-e-sistemas" + When adiciono as metas "Requisitos" e "Testes de software" para a turma "engenharia-de-software-e-sistemas" + And deleto a meta "Testes de software" na turma "engenharia-de-software-e-sistemas" + And eu submeto a criação de metas + Then vejo a notificação "Metas criadas com sucesso!" + And a listagem de metas da turma "engenharia-de-software-e-sistemas" exibe o item com título "Requisitos" + + Scenario: Turmas com metas já cadastradas não permitem criação de novas metas + Given a turma "sistemas-distribuidos" existe no sistema + And a turma "sistemas-distribuidos" possui as metas "Comunicação" e "Sincronização" cadastradas + When tento acessar a página de criação de metas da turma "sistemas-distribuidos" + Then eu vejo que não está disponível a opção de criação de metas para a turma "sistemas-distribuidos" \ No newline at end of file diff --git a/client/src/features/server-criacao-metas.feature b/client/src/features/server-criacao-metas.feature new file mode 100644 index 00000000..d3dd4098 --- /dev/null +++ b/client/src/features/server-criacao-metas.feature @@ -0,0 +1,45 @@ +@server +Feature: Gerenciamento de Metas no Servidor + Como um sistema + Eu quero gerenciar as metas das turmas através da API + Para que os dados das metas sejam armazenados e validados corretamente + + Background: + Given o servidor está disponível + + Scenario: Criar metas para uma turma via API com sucesso + Given a turma "engenharia-de-software-e-sistemas" existe no servidor + And não existem metas cadastradas para a turma "engenharia-de-software-e-sistemas" no servidor + When eu envio uma requisição para criar as metas para a turma "engenharia-de-software-e-sistemas": + | titulo | + | Requisitos | + | Testes de software | + Then a requisição deve ser aceita com sucesso + And o servidor deve conter as metas "Requisitos" e "Testes de software" associadas à turma "engenharia-de-software-e-sistemas" + + Scenario: Tentar criar meta com título vazio via API + Given a turma "engenharia-de-software-e-sistemas" existe no servidor + When eu envio uma requisição para criar as metas para a turma "engenharia-de-software-e-sistemas": + | titulo | + | | + | Testes de software | + Then a requisição deve ser rejeitada com erro de validação + And não devem existir metas cadastradas para a turma "engenharia-de-software-e-sistemas" no servidor + + Scenario: Tentar criar metas duplicadas na mesma requisição via API + Given a turma "engenharia-de-software-e-sistemas" existe no servidor + When eu envio uma requisição para criar as metas para a turma "engenharia-de-software-e-sistemas": + | titulo | + | Requisitos | + | Requisitos | + Then a requisição deve ser rejeitada com erro de conflito ou validação + And não devem existir metas cadastradas para a turma "engenharia-de-software-e-sistemas" no servidor + + Scenario: Tentar criar metas para uma turma que já possui metas + Given a turma "sistemas-distribuidos" existe no servidor + And a turma "sistemas-distribuidos" já possui as metas "Comunicação" e "Sincronização" no servidor + When eu envio uma requisição para criar as metas para a turma "sistemas-distribuidos": + | titulo | + | Nova Meta A | + Then a requisição deve ser rejeitada pois a turma já possui metas + And as metas da turma "sistemas-distribuidos" devem permanecer "Comunicação" e "Sincronização" \ No newline at end of file diff --git a/client/src/services/ClassService.ts b/client/src/services/ClassService.ts index aa9fc5fa..9615e3d5 100644 --- a/client/src/services/ClassService.ts +++ b/client/src/services/ClassService.ts @@ -1,4 +1,4 @@ -import { Class } from '../types/Class'; +import { Class, CreateClassRequest, UpdateClassRequest } from '../types/Class'; import { ReportData } from '../types/Report'; const API_BASE_URL = 'http://localhost:3005'; @@ -20,7 +20,7 @@ class ClassService { } } - static async addClass(classData: Omit): Promise { + static async addClass(classData: CreateClassRequest): Promise { try { const response = await fetch(`${API_BASE_URL}/api/classes`, { method: 'POST', @@ -42,7 +42,7 @@ class ClassService { } } - static async updateClass(classId: string, classData: Omit): Promise { + static async updateClass(classId: string, classData: UpdateClassRequest): Promise { try { const response = await fetch(`${API_BASE_URL}/api/classes/${classId}`, { method: 'PUT', @@ -96,6 +96,27 @@ class ClassService { } } + static async setMetas(classId: string, metas: string[]): Promise { + try { + const response = await fetch(`${API_BASE_URL}/api/classes/${classId}/metas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ metas }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to set metas'); + } + + return data; + } catch (error) { + console.error('Error setting metas:', error); + throw error; + } + } + } export default ClassService; \ No newline at end of file diff --git a/client/src/step-definitions/criacao-metas.steps.ts b/client/src/step-definitions/criacao-metas.steps.ts new file mode 100644 index 00000000..23c3b85b --- /dev/null +++ b/client/src/step-definitions/criacao-metas.steps.ts @@ -0,0 +1,317 @@ +import { Given, When, Then, After } from '@cucumber/cucumber'; +import expect from 'expect'; +import { scope } from './setup'; + +const BASE = 'http://localhost:3004'; +const API = 'http://localhost:3005'; +const createdClassIds: string[] = []; +let currentOpenTopic = ''; + +const SELECTORS = { + classesTab: '[data-testid="classes-tab"]', + tableRows: 'table tbody tr', + modal: '.enrollment-overlay .enrollment-modal', + metaInput: '.enrollment-overlay .enrollment-modal input[placeholder="New meta"]', + metaAddBtn: '.meta-add-btn', + metaCreateBtn: '.meta-create-btn', + modalClose: '.modal-close-btn', + metaItems: '.local-metas-list li, .existing-metas li, .local-meta-item, .local-metas-list span', + notification: '.success-message, .alert-success, [role="alert"], [aria-live]' +}; + +function toTopic(slug: string) { return slug.replace(/-/g, ' '); } +async function sleep(ms = 200) { return new Promise(r => setTimeout(r, ms)); } + +async function clickIf(selector: string, opts = { timeout: 1000 }) { + const el = await scope.page.waitForSelector(selector, { timeout: opts.timeout }).catch(() => null); + if (el) await el.click(); + return el; +} + +async function openMetasModalForTopic(topic: string) { + // avoid reopening if already open for same topic + if (currentOpenTopic === topic) return; + + // if modal open for another topic, close it first + const existingModal = await scope.page.$(SELECTORS.modal); + if (existingModal && currentOpenTopic && currentOpenTopic !== topic) { + const closeBtn = await existingModal.$(SELECTORS.modalClose); + if (closeBtn) await closeBtn.click().catch(() => {}); + currentOpenTopic = ''; + await scope.page.waitForSelector(SELECTORS.modal, { timeout: 1000 }).catch(() => {}); + } + + await scope.page.goto(BASE, { waitUntil: 'networkidle2' }); + await clickIf(SELECTORS.classesTab, { timeout: 2000 }); + await scope.page.waitForSelector(SELECTORS.tableRows, { timeout: 2000 }); + const rows = await scope.page.$$(SELECTORS.tableRows); + for (const row of rows) { + const head = await row.$('td strong'); + if (!head) continue; + const txt = (await scope.page.evaluate(el => (el.textContent || '').trim(), head)) || ''; + if (txt.toLowerCase().includes(topic.toLowerCase())) { + const btn = await row.$('.metas-btn'); + if (btn) { + await btn.click(); + await scope.page.waitForSelector(SELECTORS.modal, { timeout: 2000 }); + currentOpenTopic = topic; + } + break; + } + } +} + +async function ensureFreshClass(topic: string) { + try { + const resp = await fetch(`${API}/api/classes`); + if (!resp.ok) return; + const list = await resp.json(); + const matches = list.filter((c: any) => ((c.topic || '').toLowerCase() === topic.toLowerCase())); + for (const m of matches) { + await fetch(`${API}/api/classes/${encodeURIComponent(m.id)}`, { method: 'DELETE' }).catch(() => {}); + } + const create = await fetch(`${API}/api/classes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ topic, semester: 1, year: new Date().getFullYear() }) + }); + if (create.ok) { + const c = await create.json(); + if (c && c.id) createdClassIds.push(c.id); + } + } catch (e) { /* ignore network issues in setup */ } +} + +async function addMetasToModal(titles: string[]) { + for (const title of titles) { + await scope.page.waitForSelector(SELECTORS.metaInput, { timeout: 1500 }); + await scope.page.click(SELECTORS.metaInput, { clickCount: 3 }); + await scope.page.focus(SELECTORS.metaInput); + await scope.page.keyboard.down('Control'); await scope.page.keyboard.press('A'); await scope.page.keyboard.up('Control'); + await scope.page.keyboard.press('Backspace'); + await scope.page.type(SELECTORS.metaInput, title); + await scope.page.click(SELECTORS.metaAddBtn).catch(() => {}); + // wait until a metas item with the title appears + await scope.page.waitForFunction((t, sel) => Array.from(document.querySelectorAll(sel)).some(n => (n.textContent||'').includes(t)), { timeout: 1500 }, title, SELECTORS.metaItems).catch(() => {}); + await sleep(100); + } +} + +async function captureNotificationNow() { + const sel = await scope.page.waitForSelector(SELECTORS.notification, { timeout: 1200 }).catch(() => null); + if (!sel) return ''; + return await scope.page.evaluate(el => (el.textContent || '').trim(), sel); +} + +async function getMetasList() { + return await scope.page.evaluate((sel) => Array.from(document.querySelectorAll(sel)) + .map(n => (n.textContent || '').trim()) + .filter(t => t && t !== 'No local metas added yet'), SELECTORS.metaItems) + .catch(() => []); +} + +After({ tags: '@gui' }, async function () { + for (const id of createdClassIds) { + try { await fetch(`${API}/api/classes/${encodeURIComponent(id)}`, { method: 'DELETE' }); } catch {} + } + currentOpenTopic = ''; +}); + +Given('a turma {string} existe no sistema', async function (slug: string) { + await ensureFreshClass(toTopic(slug)); +}); + +Given('estou na página de criação de metas da turma {string}', async function (slug: string) { + await openMetasModalForTopic(toTopic(slug)); + await scope.page.waitForSelector(SELECTORS.metaInput, { timeout: 2000 }); +}); + +Given('não existe nenhuma meta cadastrada na turma {string}', async function (slug: string) { + const topic = toTopic(slug); + await openMetasModalForTopic(topic); + const modal = await scope.page.$(SELECTORS.modal); + if (modal) { + const closeBtn = await modal.$(SELECTORS.modalClose); + if (closeBtn) await closeBtn.click(); + currentOpenTopic = ''; + } + await ensureFreshClass(topic); +}); + +When('adiciono as metas {string} e {string} para a turma {string}', async function (t1: string, t2: string, slug: string) { + await openMetasModalForTopic(toTopic(slug)); + await addMetasToModal([t1, t2]); +}); + +When('eu submeto a criação de metas', async function () { + const modal = await scope.page.$(SELECTORS.modal); + if (!modal) throw new Error('Modal não está aberto'); + const createBtn = await modal.$(SELECTORS.metaCreateBtn); + if (!createBtn) throw new Error('Botão de criação de metas não encontrado.'); + await createBtn.click(); + // GUI-only notification capture (short wait) + (this as any).lastNotification = await captureNotificationNow(); + // modal likely closed after submit + currentOpenTopic = ''; +}); + +Then('vejo a notificação {string}', async function (expected: string) { + const captured: string = (this as any).lastNotification || ''; + if (captured === expected) return expect(true).toBeTruthy(); + // fallback: quick scan of body + try { + await scope.page.waitForFunction((exp) => document.body && (document.body.innerText || '').includes(exp), { timeout: 1000 }, expected); + return expect(true).toBeTruthy(); + } catch {} + throw new Error(`Notificação não encontrada na GUI. Esperado: "${expected}", Capturado: "${captured}"`); +}); + +Then('a listagem de metas da turma {string} exibe os itens com títulos {string} e {string}', async function (slug: string, t1: string, t2: string) { + const topic = toTopic(slug); + let metas: string[] = []; + for (let i = 0; i < 3; i++) { + await openMetasModalForTopic(topic); + metas = await getMetasList(); + if (metas.some(m => m.includes(t1)) && metas.some(m => m.includes(t2))) return; + await sleep(500); + } + throw new Error(`Metas não encontradas. Esperado: "${t1}", "${t2}". Lista atual: [${metas.join(', ')}]`); +}); + +When('tento adicionar uma meta sem título para a turma {string}', async function (slug: string) { + const topic = toTopic(slug); + await openMetasModalForTopic(topic); + const modal = await scope.page.$(SELECTORS.modal); + if (!modal) throw new Error('Modal não aberto'); + const input = await modal.$('input[placeholder="New meta"]'); + if (!input) throw new Error('Input de meta não encontrado'); + // ensure empty + await input.click({ clickCount: 3 }); + await input.focus(); + await scope.page.keyboard.down('Control'); await scope.page.keyboard.press('A'); await scope.page.keyboard.up('Control'); await scope.page.keyboard.press('Backspace'); + const addBtn = await modal.$(SELECTORS.metaAddBtn); + if (addBtn) await addBtn.click(); + // store state for next assertion + const createBtn = await modal.$(SELECTORS.metaCreateBtn); + const disabled = createBtn ? await scope.page.evaluate(el => (el as HTMLButtonElement).disabled, createBtn) : true; + (this as any).lastCreateDisabled = disabled; +}); + +Then('eu vejo que não está disponível a opção de submissão de criação de metas', async function () { + const disabled = (this as any).lastCreateDisabled; + if (disabled) return expect(true).toBeTruthy(); + // fallback: check currently on screen + const createBtn = await scope.page.$(SELECTORS.metaCreateBtn); + const isDisabled = createBtn ? await scope.page.evaluate(el => (el as HTMLButtonElement).disabled, createBtn) : true; + if (isDisabled) return expect(true).toBeTruthy(); + throw new Error('Opção de submissão de criação de metas está disponível'); +}); + +Then('a listagem de metas da turma {string} permanece vazia', async function (slug: string) { + const topic = toTopic(slug); + await openMetasModalForTopic(topic); + const metas = await getMetasList(); + if (metas.length === 0) return expect(true).toBeTruthy(); + throw new Error(`Lista de metas não está vazia: [${metas.join(', ')}]`); +}); + +When('cancelo a criação de metas para a turma {string}', async function (slug: string) { + await openMetasModalForTopic(toTopic(slug)); + const modal = await scope.page.$(SELECTORS.modal); + if (!modal) throw new Error('Modal não aberto'); + const cancelBtn = await modal.$('.cancel-btn'); + if (cancelBtn) await cancelBtn.click(); else { + const closeBtn = await modal.$(SELECTORS.modalClose); + if (closeBtn) await closeBtn.click(); + } + // ensure modal is gone + await scope.page.waitForSelector(SELECTORS.modal, { timeout: 1000 }).catch(() => {}); + const still = await scope.page.$(SELECTORS.modal); + if (still) throw new Error('Modal ainda visível após cancelar'); + currentOpenTopic = ''; +}); + +When('edito a meta {string} para {string} na turma {string}', async function (oldTitle: string, newTitle: string, slug: string) { + // operate on currently-open modal; open only if missing + const modalPresent = await scope.page.$(SELECTORS.modal); + if (!modalPresent) await openMetasModalForTopic(toTopic(slug)); + // find local meta item containing oldTitle + const handles = await scope.page.$$(SELECTORS.metaItems); + let found = false; + for (const h of handles) { + const txt = (await scope.page.evaluate(el => (el.textContent || '').trim(), h)); + if (txt.includes(oldTitle)) { + found = true; + const editBtn = await h.$('.meta-edit-btn'); + if (!editBtn) throw new Error('Botão de editar não encontrado'); + await editBtn.click(); + await scope.page.waitForFunction((val) => { + const input = document.querySelector('.enrollment-overlay .enrollment-modal input[placeholder="New meta"]') as HTMLInputElement; + return input && input.value === val; + }, { timeout: 1000 }, oldTitle); + const input = await scope.page.$(SELECTORS.metaInput); + if (!input) throw new Error('Input de meta não encontrado ao editar'); + await input.click({ clickCount: 3 }); + await input.focus(); + await scope.page.keyboard.down('Control'); await scope.page.keyboard.press('A'); await scope.page.keyboard.up('Control'); await scope.page.keyboard.press('Backspace'); + await input.type(newTitle); + const saveBtn = await scope.page.$(SELECTORS.metaAddBtn); + if (saveBtn) await saveBtn.click(); + // wait for notification or similar feedback + await sleep(500); + // ensure modal closed + currentOpenTopic = ''; + break; + } + } + if (!found) throw new Error(`Meta com título "${oldTitle}" não encontrada para edição`); +}); + +When('deleto a meta {string} na turma {string}', async function (title: string, slug: string) { + const modalPresent = await scope.page.$(SELECTORS.modal); + if (!modalPresent) await openMetasModalForTopic(toTopic(slug)); + const items = await scope.page.$$(SELECTORS.metaItems); + let found = false; + for (const item of items) { + const txt = (await scope.page.evaluate(el => (el.textContent || '').trim(), item)); + if (txt.includes(title)) { + found = true; + const del = await item.$('.meta-delete-btn'); + if (!del) throw new Error('Botão de deletar não encontrado'); + await del.click(); + await sleep(200); + break; + } + } + if (!found) throw new Error('Meta para deletar não encontrada: ' + title); +}); + +Then('a listagem de metas da turma {string} exibe o item com título {string}', async function (slug: string, title: string) { + await openMetasModalForTopic(toTopic(slug)); + const metas = await getMetasList(); + if (metas.some(m => m.includes(title))) return expect(true).toBeTruthy(); + throw new Error('Meta não encontrada: ' + title); +}); + +Given('a turma {string} possui as metas {string} e {string} cadastradas', async function (slug: string, m1: string, m2: string) { + const topic = toTopic(slug); + await ensureFreshClass(topic); + const resp = await fetch(`${API}/api/classes`); + const list = resp.ok ? await resp.json() : []; + const cls = list.find((c:any)=> (c.topic || '').toLowerCase() === topic.toLowerCase()); + if (!cls) throw new Error('Classe não encontrada: ' + topic); + await fetch(`${API}/api/classes/${encodeURIComponent(cls.id)}/metas`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ metas: [m1,m2] }) }); +}); + +When('tento acessar a página de criação de metas da turma {string}', async function (slug: string) { + await openMetasModalForTopic(toTopic(slug)); +}); + +Then('eu vejo que não está disponível a opção de criação de metas para a turma {string}', async function (slug: string) { + await openMetasModalForTopic(toTopic(slug)); + const createBtn = await scope.page.$(SELECTORS.metaCreateBtn); + if (!createBtn) return expect(true).toBeTruthy(); + const disabled = await scope.page.evaluate(el => (el as HTMLButtonElement).disabled, createBtn); + if (disabled) return expect(true).toBeTruthy(); + throw new Error('Opção de criação de metas está disponível para turma com metas existentes'); +}); \ No newline at end of file diff --git a/client/src/step-definitions/server-criacao-metas.steps.ts b/client/src/step-definitions/server-criacao-metas.steps.ts new file mode 100644 index 00000000..1c6e1854 --- /dev/null +++ b/client/src/step-definitions/server-criacao-metas.steps.ts @@ -0,0 +1,184 @@ +import { Given, When, Then, DataTable } from '@cucumber/cucumber'; +import expect from 'expect'; + +// Mocking axios since it's not installed +const axios = { + get: async (url: string) => { + const res = await fetch(url); + if (!res.ok) throw new Error(`Request failed with status ${res.status}`); + return { data: await res.json(), status: res.status }; + }, + post: async (url: string, data: any) => { + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!res.ok) { + const error: any = new Error(`Request failed with status ${res.status}`); + error.response = { status: res.status }; + throw error; + } + // Handle empty response body (e.g. 204 No Content) or non-JSON response + const text = await res.text(); + const dataJson = text ? JSON.parse(text) : {}; + return { data: dataJson, status: res.status }; + }, + delete: async (url: string) => { + const res = await fetch(url, { method: 'DELETE' }); + if (!res.ok) throw new Error(`Request failed with status ${res.status}`); + return { status: res.status }; + }, + isAxiosError: (error: any) => !!error.response +}; + +const serverUrl = 'http://localhost:3005/api'; // Updated to include /api prefix based on other files +let lastResponse: any | undefined; +let lastError: any | undefined; + +// --- Helpers --- + +async function resetClass(turmaId: string) { + try { + // First, find the class ID by topic + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + + if (cls) { + // Delete the class using its ID + await axios.delete(`${serverUrl}/classes/${encodeURIComponent(cls.id)}`); + } + } catch (error) { + // Ignore if class doesn't exist or fetch fails + } + + // Recreate the class without goals + try { + await axios.post(`${serverUrl}/classes`, { + topic: turmaId.replace(/-/g, ' '), + semester: 1, + year: new Date().getFullYear() + }); + } catch (error) { + // Ignore if creation fails + } +} + +// --- Step Definitions --- + +Given('o servidor está disponível', async function () { + try { + await axios.get(`${serverUrl}/classes`); + } catch (error) { + console.warn('Aviso: Servidor pode não estar disponível.'); + } +}); + +Given('a turma {string} existe no servidor', async function (turmaId: string) { + // Ensure class exists (create if not) + try { + // Always reset class to ensure clean state for each scenario + await resetClass(turmaId); + } catch (e) {} +}); + +Given('não existem metas cadastradas para a turma {string} no servidor', async function (turmaId: string) { + // Since goals are permanent, we MUST recreate the class to ensure no goals + await resetClass(turmaId); +}); + +Given('a turma {string} já possui as metas {string} e {string} no servidor', async function (turmaId: string, meta1: string, meta2: string) { + // Reset class to ensure clean state + await resetClass(turmaId); + + // Find the class ID (since we recreate it, ID might change or we use topic as ID lookup) + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + + if (cls) { + // Add goals + await axios.post(`${serverUrl}/classes/${cls.id}/metas`, { + metas: [meta1, meta2] + }); + } +}); + +When('eu envio uma requisição para criar as metas para a turma {string}:', async function (turmaId: string, dataTable: DataTable) { + const metas = dataTable.hashes().map(row => row.titulo); // API expects array of strings, not objects + + lastResponse = undefined; + lastError = undefined; + + try { + // Need to find class ID first + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + + if (cls) { + lastResponse = await axios.post(`${serverUrl}/classes/${cls.id}/metas`, { + metas: metas + }); + } else { + throw new Error(`Class ${turmaId} not found`); + } + } catch (error) { + // Capture error for validation steps + lastError = error; + // Do not rethrow if we expect an error (which we do in some scenarios) + // But if it's a network error or unexpected, it might be good to know. + // For now, we store it in lastError and let the Then steps assert it. + } +}); + +Then('a requisição deve ser aceita com sucesso', function () { + expect(lastResponse).toBeDefined(); + expect([200, 201]).toContain(lastResponse?.status); +}); + +Then('a requisição deve ser rejeitada com erro de validação', function () { + expect(lastError).toBeDefined(); + // Accept 400 (Bad Request) or 409 (Conflict) or 422 (Unprocessable Entity) + expect([400, 409, 422]).toContain(lastError?.response?.status); +}); + +Then('a requisição deve ser rejeitada com erro de conflito ou validação', function () { + expect(lastError).toBeDefined(); + expect([400, 409]).toContain(lastError?.response?.status); +}); + +Then('a requisição deve ser rejeitada pois a turma já possui metas', function () { + expect(lastError).toBeDefined(); + expect([400, 403, 409]).toContain(lastError?.response?.status); +}); + +Then('o servidor deve conter as metas {string} e {string} associadas à turma {string}', async function (meta1: string, meta2: string, turmaId: string) { + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + + expect(cls).toBeDefined(); + // Assuming the class object contains the metas or we need to fetch them + // Based on other files, it seems metas might be part of the class object or fetched separately? + // Let's assume they are in the class object for now or fetch if needed. + // If the API returns metas in the class object: + const metas = cls.metas || []; + // If metas are strings: + expect(metas).toContain(meta1); + expect(metas).toContain(meta2); +}); + +Then('não devem existir metas cadastradas para a turma {string} no servidor', async function (turmaId: string) { + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + const metas = cls ? (cls.metas || []) : []; + expect(metas).toHaveLength(0); +}); + +Then('as metas da turma {string} devem permanecer {string} e {string}', async function (turmaId: string, meta1: string, meta2: string) { + const res = await axios.get(`${serverUrl}/classes`); + const cls = res.data.find((c: any) => (c.topic || '').toLowerCase() === turmaId.replace(/-/g, ' ').toLowerCase()); + const metas = cls ? (cls.metas || []) : []; + + expect(metas).toHaveLength(2); + expect(metas).toContain(meta1); + expect(metas).toContain(meta2); +}); \ No newline at end of file diff --git a/client/src/step-definitions/server-student-steps.ts b/client/src/step-definitions/server-student-steps.ts index bd994ca6..5b028626 100644 --- a/client/src/step-definitions/server-student-steps.ts +++ b/client/src/step-definitions/server-student-steps.ts @@ -2,7 +2,7 @@ import { Given, When, Then, After, DataTable, setDefaultTimeout } from '@cucumbe import expect from 'expect'; // Set default timeout for all steps -setDefaultTimeout(30 * 1000); // 30 seconds +// setDefaultTimeout(30 * 1000); // 30 seconds const serverUrl = 'http://localhost:3005'; diff --git a/client/src/step-definitions/setup.ts b/client/src/step-definitions/setup.ts new file mode 100644 index 00000000..f0ebfce5 --- /dev/null +++ b/client/src/step-definitions/setup.ts @@ -0,0 +1,26 @@ +import { Before, After, setDefaultTimeout } from '@cucumber/cucumber'; +import { Browser, Page, launch } from 'puppeteer'; + +setDefaultTimeout(30 * 1000); + +export const scope = { + browser: null as unknown as Browser, + page: null as unknown as Page, +}; + +Before({ tags: '@gui' }, async function () { + scope.browser = await launch({ + headless: false, + slowMo: 50, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + const pages = await scope.browser.pages(); + scope.page = pages.length > 0 ? pages[0] : await scope.browser.newPage(); + await scope.page.setViewport({ width: 1280, height: 720 }); +}); + +After({ tags: '@gui' }, async function () { + if (scope.browser) { + await scope.browser.close(); + } +}); diff --git a/client/src/step-definitions/student-steps.ts b/client/src/step-definitions/student-steps.ts index a2c3af79..1da5076e 100644 --- a/client/src/step-definitions/student-steps.ts +++ b/client/src/step-definitions/student-steps.ts @@ -1,9 +1,7 @@ import { Given, When, Then, Before, After, DataTable, setDefaultTimeout } from '@cucumber/cucumber'; import { Browser, Page, launch } from 'puppeteer'; import expect from 'expect'; - -// Set default timeout for all steps -setDefaultTimeout(30 * 1000); // 30 seconds +import { scope } from './setup'; // Helper function to format CPF like the frontend does function formatCPF(value: string): string { @@ -14,41 +12,30 @@ function formatCPF(value: string): string { return digits.slice(0, 11).replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); } -let browser: Browser; -let page: Page; const baseUrl = 'http://localhost:3004'; const serverUrl = 'http://localhost:3005'; // Test data to clean up let testStudentCPF: string; -Before({ tags: '@gui' }, async function () { - browser = await launch({ - headless: false, // Set to true for CI/CD - slowMo: 50 // Slow down actions for visibility - }); - page = await browser.newPage(); - await page.setViewport({ width: 1280, height: 720 }); -}); - After({ tags: '@gui' }, async function () { // Clean up test student if it exists by using the GUI delete function if (testStudentCPF) { try { // Navigate to Students area - await page.goto(baseUrl); - await page.waitForSelector('.students-list table', { timeout: 5000 }); + await scope.page.goto(baseUrl); + await scope.page.waitForSelector('.students-list table', { timeout: 5000 }); // Look for our test student in the table and delete it if found - const studentRows = await page.$$('[data-testid^="student-row-"]'); + const studentRows = await scope.page.$$('[data-testid^="student-row-"]'); for (const row of studentRows) { const cpfCell = await row.$('[data-testid="student-cpf"]'); if (cpfCell) { - const cpf = await page.evaluate(el => el.textContent, cpfCell); + const cpf = await scope.page.evaluate(el => el.textContent, cpfCell); // Check for both plain and formatted CPF if (cpf === testStudentCPF || cpf === formatCPF(testStudentCPF)) { // Set up dialog handler before clicking delete - page.once('dialog', async (dialog) => { + scope.page.once('dialog', async (dialog) => { console.log(`GUI cleanup: Confirming deletion dialog: ${dialog.message()}`); await dialog.accept(); // Confirm deletion }); @@ -69,16 +56,12 @@ After({ tags: '@gui' }, async function () { console.log('GUI cleanup: Student may not exist or GUI unavailable'); } } - - if (browser) { - await browser.close(); - } }); Given('the student management system is running', async function () { - await page.goto(baseUrl); - await page.waitForSelector('h1', { timeout: 10000 }); - const title = await page.$eval('h1', el => el.textContent); + await scope.page.goto(baseUrl); + await scope.page.waitForSelector('h1', { timeout: 10000 }); + const title = await scope.page.$eval('h1', el => el.textContent); expect(title || '').toContain('Teaching Assistant React'); }); @@ -96,173 +79,217 @@ Given('there is no student with CPF {string} in the system', async function (cpf const formattedCPF = formatCPF(cpf); // Navigate to the application and check if student exists through GUI - await page.goto(baseUrl); - await page.waitForSelector('.students-list', { timeout: 10000 }); + await scope.page.goto(baseUrl); + await scope.page.waitForSelector('.students-list', { timeout: 10000 }); - // Try to find and delete the student if it exists (cleanup before test) - const studentRows = await page.$$('[data-testid^="student-row-"]'); - for (const row of studentRows) { - const cpfCell = await row.$('[data-testid="student-cpf"]'); - if (cpfCell) { - const displayedCPF = await page.evaluate(el => el.textContent, cpfCell); - // Check for both plain and formatted CPF - if (displayedCPF === cpf || displayedCPF === formattedCPF) { - // Student exists, delete it for clean test state - // Set up dialog handler before clicking delete - page.once('dialog', async (dialog) => { - console.log(`GUI cleanup: Confirming deletion dialog: ${dialog.message()}`); - await dialog.accept(); // Confirm deletion - }); - - const deleteButton = await row.$(`[data-testid="delete-student-${displayedCPF}"]`); - if (deleteButton) { - await deleteButton.click(); - // Wait for deletion to complete - await new Promise(resolve => setTimeout(resolve, 1000)); - console.log(`GUI cleanup: Removed existing student with CPF: ${displayedCPF}`); - break; - } + // Check if student exists in the table + const studentExists = await scope.page.evaluate((targetCpf) => { + const rows = document.querySelectorAll('[data-testid^="student-row-"]'); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const cpfCell = row.querySelector('[data-testid="student-cpf"]'); + if (cpfCell && (cpfCell.textContent === targetCpf)) { + return true; } } - } + return false; + }, formattedCPF); - // Verify student doesn't exist by checking the GUI - await page.reload(); // Refresh to ensure clean state - await page.waitForSelector('.students-list', { timeout: 5000 }); - - const updatedRows = await page.$$('[data-testid^="student-row-"]'); - for (const row of updatedRows) { - const cpfCell = await row.$('[data-testid="student-cpf"]'); - if (cpfCell) { - const displayedCPF = await page.evaluate(el => el.textContent, cpfCell); - if (displayedCPF === cpf || displayedCPF === formattedCPF) { - throw new Error(`Student with CPF ${displayedCPF} still exists in the system after cleanup`); - } + if (studentExists) { + // Delete the student if found + scope.page.once('dialog', async (dialog) => { + await dialog.accept(); + }); + + const deleteButton = await scope.page.$(`[data-testid="delete-student-${formattedCPF}"]`); + if (deleteButton) { + await deleteButton.click(); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for deletion } } }); +When('I navigate to the Students page', async function () { + // Click on the Students tab/link + const studentsLink = await scope.page.$('a[href="/students"]'); + if (studentsLink) { + await studentsLink.click(); + } else { + // Fallback to direct navigation + await scope.page.goto(`${baseUrl}/students`); + } + await scope.page.waitForSelector('.students-list', { timeout: 5000 }); +}); + When('I navigate to the Students area', async function () { - // Click on the Students tab - const studentsTab = await page.$('[data-testid="students-tab"]'); - if (studentsTab) { - const isActive = await page.evaluate(el => el?.classList.contains('active'), studentsTab); - - if (!isActive) { - await studentsTab.click(); - } + // Click on the Students tab/link + const studentsLink = await scope.page.$('a[href="/students"]'); + if (studentsLink) { + await studentsLink.click(); + } else { + // Fallback to direct navigation + await scope.page.goto(`${baseUrl}/students`); } + await scope.page.waitForSelector('.students-list', { timeout: 5000 }); +}); + +When('I enter the student details:', async function (dataTable: DataTable) { + const data = dataTable.rowsHash(); - // Wait for the student form to be visible - await page.waitForSelector('[data-testid="student-form"]', { timeout: 5000 }); + // Fill in the form + await scope.page.type('input[name="name"]', data.name); + await scope.page.type('input[name="cpf"]', data.cpf); + await scope.page.type('input[name="email"]', data.email); }); When('I provide the student information:', async function (dataTable: DataTable) { const data = dataTable.rowsHash(); - // Fill in the name field using semantic ID - await page.waitForSelector('#name'); - await page.click('#name'); - await page.type('#name', data.name); - - // Fill in the CPF field using semantic ID - await page.click('#cpf'); - await page.type('#cpf', data.cpf); - - // Fill in the email field using semantic ID - await page.click('#email'); - await page.type('#email', data.email); + // Fill in the form + await scope.page.type('input[name="name"]', data.name); + await scope.page.type('input[name="cpf"]', data.cpf); + await scope.page.type('input[name="email"]', data.email); }); -When('I send the student information', async function () { - // Click the submit button using semantic test ID - const submitButton = await page.$('[data-testid="submit-student-button"]'); - expect(submitButton).toBeTruthy(); +When('I click the {string} button', async function (buttonText: string) { + // Find button by text content + const button = await scope.page.evaluateHandle((text) => { + const buttons = Array.from(document.querySelectorAll('button')); + return buttons.find(b => b.textContent?.includes(text)); + }, buttonText); - await submitButton?.click(); - - // Wait for the information to be processed and student to appear - await new Promise(resolve => setTimeout(resolve, 2000)); + if (button) { + const element = button.asElement(); + if (element) { + await (element as any).click(); + } + } else { + throw new Error(`Button with text "${buttonText}" not found`); + } }); -Then('I should see {string} in the student list', async function (studentName: string) { - // Wait for the student list to update - await page.waitForSelector('.students-list table', { timeout: 10000 }); - - // Find the student row that matches our test student's CPF and verify the name - const studentRows = await page.$$('[data-testid^="student-row-"]'); - let foundStudent = null; - - for (const row of studentRows) { - const cpfCell = await row.$('[data-testid="student-cpf"]'); - if (cpfCell) { - const cpf = await page.evaluate(el => el.textContent, cpfCell); - if (cpf === formatCPF(testStudentCPF) || cpf === testStudentCPF) { - foundStudent = row; - break; - } +When('I send the student information', async function () { + const registerButton = await scope.page.$('button[type="submit"]'); + if (registerButton) { + await registerButton.click(); + // Wait for the student list to update or a success message, instead of navigation + // Assuming the list updates on the same page + try { + await scope.page.waitForFunction( + () => document.querySelectorAll('[data-testid^="student-row-"]').length > 0, + { timeout: 5000 } + ); + } catch (e) { + // Ignore timeout if list doesn't update immediately, subsequent steps will verify } + } else { + throw new Error('Register button not found'); } +}); + +Then('I should see the student {string} in the list', async function (name: string) { + await scope.page.waitForFunction( + (studentName) => { + const rows = document.querySelectorAll('[data-testid^="student-row-"]'); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row.textContent?.includes(studentName)) { + return true; + } + } + return false; + }, + { timeout: 5000 }, + name + ); +}); + +Then('I should see {string} in the student list', async function (name: string) { + await scope.page.waitForFunction( + (studentName) => { + const rows = document.querySelectorAll('[data-testid^="student-row-"]'); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row.textContent?.includes(studentName)) { + return true; + } + } + return false; + }, + { timeout: 5000 }, + name + ); +}); + +Then('the student should have CPF {string}', async function (cpf: string) { + const formattedCPF = formatCPF(cpf); + const cpfFound = await scope.page.evaluate((targetCpf) => { + const cells = document.querySelectorAll('[data-testid="student-cpf"]'); + return Array.from(cells).some(cell => cell.textContent === targetCpf); + }, formattedCPF); - expect(foundStudent).toBeTruthy(); + expect(cpfFound).toBe(true); +}); + +Then('the student should have email {string}', async function (email: string) { + const emailFound = await scope.page.evaluate((targetEmail) => { + const cells = document.querySelectorAll('[data-testid="student-email"]'); + return Array.from(cells).some(cell => cell.textContent === targetEmail); + }, email); - // Verify the name matches exactly for this specific student - const nameCell = await foundStudent!.$('[data-testid="student-name"]'); - const actualName = await page.evaluate(el => el.textContent, nameCell!); - expect(actualName).toBe(studentName); + expect(emailFound).toBe(true); }); -Then('the student should have CPF {string}', async function (expectedCPF: string) { - // Wait for the student list to update - await page.waitForSelector('.students-list table', { timeout: 10000 }); +When('I click the delete button for student with CPF {string}', async function (cpf: string) { + const formattedCPF = formatCPF(cpf); - // Find all student information from the current test student - const studentRows = await page.$$('[data-testid^="student-row-"]'); - let foundStudent = null; + // Setup dialog handler + scope.page.once('dialog', async (dialog) => { + await dialog.accept(); + }); - // First, find the student row that matches our test CPF - for (const row of studentRows) { - const cpfCell = await row.$('[data-testid="student-cpf"]'); - if (cpfCell) { - const cpf = await page.evaluate(el => el.textContent, cpfCell); - if (cpf === expectedCPF || cpf === testStudentCPF || cpf === formatCPF(testStudentCPF)) { - foundStudent = row; - break; - } - } + const deleteButton = await scope.page.$(`[data-testid="delete-student-${formattedCPF}"]`); + if (!deleteButton) { + throw new Error(`Delete button for student ${formattedCPF} not found`); } - expect(foundStudent).toBeTruthy(); - - // Verify the CPF matches exactly - const cpfCell = await foundStudent!.$('[data-testid="student-cpf"]'); - const actualCPF = await page.evaluate(el => el.textContent, cpfCell!); - expect(actualCPF).toBe(expectedCPF); + await deleteButton.click(); }); -Then('the student should have email {string}', async function (expectedEmail: string) { - // Wait for the student list to update - await page.waitForSelector('.students-list table', { timeout: 10000 }); +Then('I should not see the student with CPF {string} in the list', async function (cpf: string) { + const formattedCPF = formatCPF(cpf); - // Find the student row that matches our test student's CPF - const studentRows = await page.$$('[data-testid^="student-row-"]'); - let foundStudent = null; + // Wait for element to disappear + await scope.page.waitForFunction( + (targetCpf) => { + const cells = document.querySelectorAll('[data-testid="student-cpf"]'); + return !Array.from(cells).some(cell => cell.textContent === targetCpf); + }, + { timeout: 5000 }, + formattedCPF + ); +}); + +When('I try to register a student with incomplete details:', async function (dataTable: DataTable) { + const data = dataTable.rowsHash(); - for (const row of studentRows) { - const cpfCell = await row.$('[data-testid="student-cpf"]'); - if (cpfCell) { - const cpf = await page.evaluate(el => el.textContent, cpfCell); - if (cpf === formatCPF(testStudentCPF) || cpf === testStudentCPF) { - foundStudent = row; - break; - } - } - } + // Fill in only provided fields + if (data.name) await scope.page.type('input[name="name"]', data.name); + if (data.cpf) await scope.page.type('input[name="cpf"]', data.cpf); + if (data.email) await scope.page.type('input[name="email"]', data.email); - expect(foundStudent).toBeTruthy(); + // Click register + const registerButton = await scope.page.$('button[type="submit"]'); + if (registerButton) await registerButton.click(); +}); + +Then('I should see an error message', async function () { + // Check for HTML5 validation or custom error message + // This is a simplified check - in a real app you'd check specific error elements + const hasError = await scope.page.evaluate(() => { + const inputs = document.querySelectorAll('input:invalid'); + return inputs.length > 0; + }); - // Verify the email matches exactly for this specific student - const emailCell = await foundStudent!.$('[data-testid="student-email"]'); - const actualEmail = await page.evaluate(el => el.textContent, emailCell!); - expect(actualEmail).toBe(expectedEmail); + expect(hasError).toBe(true); }); \ No newline at end of file diff --git a/client/src/types/Class.ts b/client/src/types/Class.ts index adb818d8..e4e53a6f 100644 --- a/client/src/types/Class.ts +++ b/client/src/types/Class.ts @@ -6,6 +6,8 @@ export interface Class { topic: string; semester: number; year: number; + metas: string[]; + metasLocked?: boolean; especificacaoDoCalculoDaMedia: EspecificacaoDoCalculoDaMedia; enrollments: Enrollment[]; } @@ -15,12 +17,14 @@ export interface CreateClassRequest { semester: number; year: number; especificacaoDoCalculoDaMedia: EspecificacaoDoCalculoDaMedia; + metas?: string[]; } export interface UpdateClassRequest { topic?: string; semester?: number; year?: number; + metas?: string[]; } // Helper function to generate class ID diff --git a/package-lock.json b/package-lock.json index 5f18ab05..012c33f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,11 +26,13 @@ "react-scripts": "5.0.1" }, "devDependencies": { - "@cucumber/cucumber": "^12.2.0", + "@cucumber/cucumber": "^12.3.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/cucumber": "^6.0.1", "@types/jest": "^30.0.0", + "@types/node": "^24.10.1", "@types/puppeteer": "^5.4.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", @@ -10311,16 +10313,6 @@ "node": ">=10" } }, - "client/node_modules/semver": { - "version": "7.7.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "client/node_modules/send": { "version": "0.19.0", "license": "MIT", @@ -12862,31 +12854,29 @@ } }, "node_modules/@cucumber/ci-environment": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", - "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", - "dev": true, - "license": "MIT" + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-12.0.0.tgz", + "integrity": "sha512-SqCEnbCNl3zCXCFpqGUuoaSNhLC0jLw4tKeFcAxTw9MD/QRlJjeAC/fyvVLFuXuSq0OunJlFfxLu+Z3HE+oLPg==", + "dev": true }, "node_modules/@cucumber/cucumber": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.2.0.tgz", - "integrity": "sha512-b7W4snvXYi1T2puUjxamASCCNhNzVSzb/fQUuGSkdjm/AFfJ24jo8kOHQyOcaoArCG71sVQci4vkZaITzl/V1w==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.3.0.tgz", + "integrity": "sha512-36cIyplE1iDl12s4k6lBVpceua8tKLklFTf7CUITPrNHTLlQ/KBr7NYUUHviPzCbj2Ox3BPTZ6qkSLd6WMvVQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@cucumber/ci-environment": "10.0.1", + "@cucumber/ci-environment": "12.0.0", "@cucumber/cucumber-expressions": "18.0.1", - "@cucumber/gherkin": "34.0.0", - "@cucumber/gherkin-streams": "5.0.1", - "@cucumber/gherkin-utils": "9.2.0", - "@cucumber/html-formatter": "21.14.0", - "@cucumber/junit-xml-formatter": "0.8.1", + "@cucumber/gherkin": "37.0.0", + "@cucumber/gherkin-streams": "6.0.0", + "@cucumber/gherkin-utils": "10.0.0", + "@cucumber/html-formatter": "22.2.0", + "@cucumber/junit-xml-formatter": "0.9.0", "@cucumber/message-streams": "4.0.1", - "@cucumber/messages": "28.1.0", + "@cucumber/messages": "31.0.0", "@cucumber/pretty-formatter": "1.0.1", - "@cucumber/tag-expressions": "6.2.0", + "@cucumber/tag-expressions": "8.1.0", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", "chalk": "^4.1.2", @@ -12895,7 +12885,7 @@ "debug": "^4.3.4", "error-stack-parser": "^2.1.4", "figures": "^3.2.0", - "glob": "^11.0.0", + "glob": "^13.0.0", "has-ansi": "^4.0.1", "indent-string": "^4.0.0", "is-installed-globally": "^0.4.0", @@ -12903,19 +12893,19 @@ "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", - "luxon": "3.7.1", + "luxon": "3.7.2", "mime": "^3.0.0", "mkdirp": "^3.0.0", "mz": "^2.7.0", "progress": "^2.0.3", - "read-package-up": "^11.0.0", - "semver": "7.7.2", + "read-package-up": "^12.0.0", + "semver": "7.7.3", "string-argv": "0.3.1", "supports-color": "^8.1.1", "type-fest": "^4.41.0", "util-arity": "^1.1.0", "yaml": "^2.2.2", - "yup": "1.7.0" + "yup": "1.7.1" }, "bin": { "cucumber-js": "bin/cucumber.js" @@ -12937,6 +12927,16 @@ "regexp-match-indices": "1.0.2" } }, + "node_modules/@cucumber/cucumber/node_modules/@cucumber/messages": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", + "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", + "dev": true, + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" + } + }, "node_modules/@cucumber/cucumber/node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -12954,24 +12954,21 @@ } }, "node_modules/@cucumber/gherkin": { - "version": "34.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", - "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-37.0.0.tgz", + "integrity": "sha512-vKJVJ6h4HCktG870wgYUUskNpFxbFI0WmAkVLPTz1LlLwJX7/KOBqFcr2/L3u0pPoHjbLRW+IpbiXLT2T13/wg==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { - "@cucumber/messages": ">=19.1.4 <29" + "@cucumber/messages": ">=31.0.0 <32" } }, "node_modules/@cucumber/gherkin-streams": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", - "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-6.0.0.tgz", + "integrity": "sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==", "dev": true, - "license": "MIT", "dependencies": { - "commander": "9.1.0", + "commander": "14.0.0", "source-map-support": "0.5.21" }, "bin": { @@ -12984,26 +12981,24 @@ } }, "node_modules/@cucumber/gherkin-streams/node_modules/commander": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", - "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, - "license": "MIT", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=20" } }, "node_modules/@cucumber/gherkin-utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", - "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-10.0.0.tgz", + "integrity": "sha512-BcujlDT343GXXNrMPl3ws6Il3zs8dQw3Yp/d3HnOJF8i2snGGgiapoTbko7MdvAt7ivDL7SDo+e1d5Cnpl3llA==", "dev": true, - "license": "MIT", "dependencies": { - "@cucumber/gherkin": "^31.0.0", - "@cucumber/messages": "^27.0.0", + "@cucumber/gherkin": "^34.0.0", + "@cucumber/messages": "^29.0.0", "@teppeis/multimaps": "3.0.0", - "commander": "13.1.0", + "commander": "14.0.0", "source-map-support": "^0.5.21" }, "bin": { @@ -13011,97 +13006,71 @@ } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { - "version": "31.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", - "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-34.0.0.tgz", + "integrity": "sha512-659CCFsrsyvuBi/Eix1fnhSheMnojSfnBcqJ3IMPNawx7JlrNJDcXYSSdxcUw3n/nG05P+ptCjmiZY3i14p+tA==", "dev": true, - "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <=26" + "@cucumber/messages": ">=19.1.4 <29" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { - "version": "26.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", - "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-28.1.0.tgz", + "integrity": "sha512-2LzZtOwYKNlCuNf31ajkrekoy2M4z0Z1QGiPH40n4gf5t8VOUFb7m1ojtR4LmGvZxBGvJZP8voOmRqDWzBzYKA==", "dev": true, - "license": "MIT", "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", "reflect-metadata": "0.2.2", - "uuid": "10.0.0" - } - }, - "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "uuid": "11.1.0" } }, "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", - "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-29.0.1.tgz", + "integrity": "sha512-aAvIYfQD6/aBdF8KFQChC3CQ1Q+GX9orlR6GurGiX6oqaCnBkxA4WU3OQUVepDynEFrPayerqKRFcAMhdcXReQ==", "dev": true, - "license": "MIT", "dependencies": { - "@types/uuid": "10.0.0", "class-transformer": "0.5.1", - "reflect-metadata": "0.2.2", - "uuid": "11.0.5" + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/gherkin-utils/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/@cucumber/gherkin-utils/node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", + "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" } }, "node_modules/@cucumber/html-formatter": { - "version": "21.14.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.14.0.tgz", - "integrity": "sha512-vQqbmQZc0QiN4c+cMCffCItpODJlOlYtPG7pH6We096dBOa7u0ttDMjT6KrMAnQlcln54rHL46r408IFpuznAw==", + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-22.2.0.tgz", + "integrity": "sha512-fUNC/KngTIz+hAQ2Yr4XjdYq+MO60PwK9SidxBQ54jNI1Vw7erlgsPq0TOWneCIvdjU3qp+YDqYG1hw3zuUuDA==", "dev": true, - "license": "MIT", "peerDependencies": { "@cucumber/messages": ">=18" } }, "node_modules/@cucumber/junit-xml-formatter": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.8.1.tgz", - "integrity": "sha512-FT1Y96pyd9/ifbE9I7dbkTCjkwEdW9C0MBobUZoKD13c8EnWAt0xl1Yy/v/WZLTk4XfCLte1DATtLx01jt+YiA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.9.0.tgz", + "integrity": "sha512-WF+A7pBaXpKMD1i7K59Nk5519zj4extxY4+4nSgv5XLsGXHDf1gJnb84BkLUzevNtp2o2QzMG0vWLwSm8V5blw==", "dev": true, - "license": "MIT", "dependencies": { - "@cucumber/query": "^13.0.2", + "@cucumber/query": "^14.0.1", "@teppeis/multimaps": "^3.0.0", "luxon": "^3.5.0", "xmlbuilder": "^15.1.1" @@ -13115,7 +13084,6 @@ "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", "dev": true, - "license": "MIT", "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" @@ -13166,11 +13134,10 @@ } }, "node_modules/@cucumber/query": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", - "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-14.6.0.tgz", + "integrity": "sha512-bPbfpkDsFCBn95erh3un76QViPqGAo3T7iYews0yA3/JRNoV009s7acxxY+f+OMABPFl0TJVIZlvqX+KayQ+Eg==", "dev": true, - "license": "MIT", "dependencies": { "@teppeis/multimaps": "3.0.0", "lodash.sortby": "^4.7.0" @@ -13180,11 +13147,10 @@ } }, "node_modules/@cucumber/tag-expressions": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.2.0.tgz", - "integrity": "sha512-KIF0eLcafHbWOuSDWFw0lMmgJOLdDRWjEL1kfXEWrqHmx2119HxVAr35WuEd9z542d3Yyg+XNqSr+81rIKqEdg==", - "dev": true, - "license": "MIT" + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-8.1.0.tgz", + "integrity": "sha512-UFeOVUyc711/E7VHjThxMwg3jbGod9TlbM1gxNixX/AGDKg82Eha4cE0tKki3GGUs7uB2NyI+hQAuhB8rL2h5A==", + "dev": true }, "node_modules/@emnapi/core": { "version": "1.7.1", @@ -13225,7 +13191,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, - "license": "MIT", "engines": { "node": "20 || >=22" } @@ -13235,7 +13200,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, - "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" }, @@ -14029,19 +13993,6 @@ "node": ">=18" } }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -14074,7 +14025,6 @@ "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } @@ -14289,6 +14239,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cucumber": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cucumber/-/cucumber-6.0.1.tgz", + "integrity": "sha512-+GZV6xfN0MeN9shDCdny8GbC8N0+U6uca8cjyaJndcwmrUhwS6qOU2vmYn0d71EOwJF568/v3SxJ8VKxuZNYRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", @@ -14426,8 +14383,7 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/puppeteer": { "version": "5.4.7", @@ -16235,7 +16191,6 @@ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -16419,22 +16374,15 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -16570,25 +16518,17 @@ } }, "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", "dev": true, - "license": "ISC", "dependencies": { - "lru-cache": "^10.0.1" + "lru-cache": "^11.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -16691,7 +16631,6 @@ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -16904,22 +16843,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -18323,21 +18246,19 @@ } }, "node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", "engines": { "node": "20 || >=22" } }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" } @@ -18499,7 +18420,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -18643,18 +18563,17 @@ "license": "MIT" }, "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "hosted-git-info": "^7.0.0", + "hosted-git-info": "^9.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -18874,7 +18793,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -18973,8 +18891,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/proxy-agent": { "version": "6.5.0", @@ -19132,38 +19049,51 @@ "license": "MIT" }, "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", "dev": true, - "license": "MIT", "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/type-fest": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "dev": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.0.0.tgz", + "integrity": "sha512-A70UlgfNdKI5NSvTTfHzLQj7NJRpJ4mT5tGafkllJ4wh71oYuGm/pzphHcmW4s35iox56KSK721AihodoXSc/A==", "dev": true, - "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.2.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -19174,7 +19104,6 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", @@ -19187,6 +19116,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", + "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "dev": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -19338,10 +19294,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "bin": { "semver": "bin/semver.js" }, @@ -19546,7 +19501,6 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -19556,15 +19510,13 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" + "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -19574,8 +19526,7 @@ "version": "3.0.22", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" + "dev": true }, "node_modules/sprintf-js": { "version": "1.0.3", @@ -19843,6 +19794,18 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tar-fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", @@ -19996,8 +19959,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/tmpl": { "version": "1.0.5", @@ -20021,8 +19983,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/tree-kill": { "version": "1.2.2", @@ -20097,19 +20058,6 @@ } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -20244,11 +20192,10 @@ "license": "MIT" }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -20396,7 +20343,6 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -20522,7 +20468,6 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.0" } @@ -20627,11 +20572,10 @@ } }, "node_modules/yup": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.0.tgz", - "integrity": "sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", "dev": true, - "license": "MIT", "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", @@ -20644,7 +20588,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, diff --git a/server/data/app-data.json b/server/data/app-data.json index fbadf223..c98cb147 100644 --- a/server/data/app-data.json +++ b/server/data/app-data.json @@ -23,9 +23,14 @@ ], "classes": [ { - "topic": "Engenharia de Software e Sistemas", + "topic": "engenharia software", "semester": 1, "year": 2025, + "metas": [ + "Requisitos", + "Testes de software" + ], + "metasLocked": false, "especificacaoDoCalculoDaMedia": { "pesosDosConceitos": { "MA": 10, @@ -41,155 +46,19 @@ "enrollments": [ { "studentCPF": "11111111111", - "evaluations": [ - { - "goal": "Requirements", - "grade": "MPA" - }, - { - "goal": "Configuration Management", - "grade": "MA" - }, - { - "goal": "Project Management", - "grade": "MANA" - }, - { - "goal": "Design", - "grade": "MA" - }, - { - "goal": "Tests", - "grade": "MPA" - }, - { - "goal": "Refactoring", - "grade": "MA" - } - ] - }, - { - "studentCPF": "55555555555", - "evaluations": [ - { - "goal": "Requirements", - "grade": "MANA" - }, - { - "goal": "Configuration Management", - "grade": "MPA" - }, - { - "goal": "Project Management", - "grade": "MPA" - }, - { - "goal": "Design", - "grade": "MA" - }, - { - "goal": "Refactoring", - "grade": "MA" - }, - { - "goal": "Tests", - "grade": "MPA" - } - ] + "evaluations": [] }, { "studentCPF": "22222222222", - "evaluations": [ - { - "goal": "Configuration Management", - "grade": "MANA" - }, - { - "goal": "Requirements", - "grade": "MA" - }, - { - "goal": "Project Management", - "grade": "MPA" - } - ] - } - ] - }, - { - "topic": "Engenharia de Software e Sistemas", - "semester": 2, - "year": 2025, - "especificacaoDoCalculoDaMedia": { - "pesosDosConceitos": { - "MA": 10, - "MPA": 7, - "MANA": 0 + "evaluations": [] }, - "pesosDasMetas": { - "Gerência de Configuração": 1, - "Gerência de Projeto": 1, - "Qualidade de Software": 1 - } - }, - "enrollments": [ { - "studentCPF": "22222222222", - "evaluations": [ - { - "goal": "Requirements", - "grade": "MPA" - }, - { - "goal": "Configuration Management", - "grade": "MA" - }, - { - "goal": "Project Management", - "grade": "MPA" - }, - { - "goal": "Design", - "grade": "MA" - }, - { - "goal": "Refactoring", - "grade": "MA" - }, - { - "goal": "Tests", - "grade": "MA" - } - ] + "studentCPF": "33333333333", + "evaluations": [] }, { - "studentCPF": "33333333333", - "evaluations": [ - { - "goal": "Requirements", - "grade": "MA" - }, - { - "goal": "Configuration Management", - "grade": "MPA" - }, - { - "goal": "Project Management", - "grade": "MANA" - }, - { - "goal": "Design", - "grade": "MA" - }, - { - "goal": "Tests", - "grade": "MANA" - }, - { - "goal": "Refactoring", - "grade": "MA" - } - ] + "studentCPF": "55555555555", + "evaluations": [] } ] } diff --git a/server/src/__tests__/metas.test.ts b/server/src/__tests__/metas.test.ts new file mode 100644 index 00000000..5a28f566 --- /dev/null +++ b/server/src/__tests__/metas.test.ts @@ -0,0 +1,59 @@ +import { Classes } from '../models/Classes'; +import { Class } from '../models/Class'; + +describe('Metas - Classes', () => { + let classes: Classes; + const specStub: any = { toJSON: () => ({}) }; + + beforeEach(() => { + classes = new Classes(); + }); + + test('deve criar metas para uma turma com sucesso', () => { + const turma = new Class('engenharia-de-software-e-sistemas', 1, 2024, [], specStub); + classes.addClass(turma); + + const metas = ['Requisitos', 'Testes de software']; + classes.addClassMetas(turma.getClassId(), metas); + + expect(classes.getClassMetas(turma.getClassId())).toEqual(metas); + expect(turma.isMetasLocked()).toBe(true); + }); + + test('deve rejeitar meta com título vazio e não persistir metas', () => { + const turma = new Class('engenharia-de-software-e-sistemas', 1, 2024, [], specStub); + classes.addClass(turma); + + expect(() => { + classes.addClassMetas(turma.getClassId(), ['', 'Testes de software']); + }).toThrow('Metas não podem ter títulos vazios!'); + + expect(classes.getClassMetas(turma.getClassId())).toEqual([]); + expect(turma.isMetasLocked()).toBe(false); + }); + + test('deve rejeitar metas duplicadas na mesma requisição e não persistir metas', () => { + const turma = new Class('engenharia-de-software-e-sistemas', 1, 2024, [], specStub); + classes.addClass(turma); + + expect(() => { + classes.addClassMetas(turma.getClassId(), ['Requisitos', 'Requisitos']); + }).toThrow('Metas não podem conter duplicatas!'); + + expect(classes.getClassMetas(turma.getClassId())).toEqual([]); + expect(turma.isMetasLocked()).toBe(false); + }); + + test('deve rejeitar tentativa de criar metas para turma que já possui metas', () => { + const metasExistentes = ['Comunicação', 'Sincronização']; + const turma = new Class('sistemas-distribuidos', 2, 2024, metasExistentes, specStub, [], true); + classes.addClass(turma); + + expect(() => { + classes.addClassMetas(turma.getClassId(), ['Nova Meta A']); + }).toThrow('Metas já foram definidas para a turma e não podem ser alteradas!'); + + expect(classes.getClassMetas(turma.getClassId())).toEqual(metasExistentes); + expect(turma.isMetasLocked()).toBe(true); + }); +}); \ No newline at end of file diff --git a/server/src/models/Class.ts b/server/src/models/Class.ts index 5e5f3800..ba9fafec 100644 --- a/server/src/models/Class.ts +++ b/server/src/models/Class.ts @@ -8,13 +8,26 @@ export class Class { private year: number; private readonly especificacaoDoCalculoDaMedia: EspecificacaoDoCalculoDaMedia; private enrollments: Enrollment[]; - - constructor(topic: string, semester: number, year: number, especificacaoDoCalculoDaMedia: EspecificacaoDoCalculoDaMedia, enrollments: Enrollment[] = []) { + private metas: string[]; + private metasLocked: boolean; + + // Update constructor to accept metasLocked (optional, default false) + constructor( + topic: string, + semester: number, + year: number, + metas: string[] = [], + especificacaoDoCalculoDaMedia: EspecificacaoDoCalculoDaMedia, + enrollments: Enrollment[] = [], + metasLocked: boolean = false // Add this parameter + ) { this.topic = topic; this.semester = semester; this.year = year; this.especificacaoDoCalculoDaMedia = especificacaoDoCalculoDaMedia; + this.metas = metas; this.enrollments = enrollments; + this.metasLocked = metasLocked; // Initialize properly } // Getters @@ -34,6 +47,14 @@ export class Class { return [...this.enrollments]; // Return copy to prevent external modification } + getMetas(): string[] { + return [...this.metas]; // Return copy to prevent external modification + } + + isMetasLocked(): boolean { + return this.metasLocked; + } + // Generate unique class ID getClassId(): string { return `${this.topic}-${this.year}-${this.semester}`; @@ -55,6 +76,27 @@ export class Class { getEspecificacaoDoCalculoDaMedia(): EspecificacaoDoCalculoDaMedia { return this.especificacaoDoCalculoDaMedia; } + + // Metas management + setMetas(metas: string[]): void { + if (this.metasLocked) { + throw new Error('Metas já foram definidas para a turma e não podem ser alteradas!'); + } + if (!Array.isArray(metas) || metas.length === 0) { + throw new Error('As metas de uma turma não devem ser vazias!'); + } + // Check for empty strings in metas + if (metas.some(m => !m || m.trim() === '')) { + throw new Error('Metas não podem ter títulos vazios!'); + } + // verificar se array tem duplicatas + const hasDuplicates = metas.length !== new Set(metas).size; + if (hasDuplicates) { + throw new Error('Metas não podem conter duplicatas!'); + } + this.metas = metas; + this.metasLocked = true; // Lock it immediately after setting + } // Enrollment management addEnrollment(student: Student): Enrollment { @@ -92,34 +134,56 @@ export class Class { getEnrolledStudents(): Student[] { return this.enrollments.map(enrollment => enrollment.getStudent()); } - - // Convert to JSON for API responses + + // Convert to JSON for API responses AND persistence toJSON() { return { id: this.getClassId(), topic: this.topic, semester: this.semester, year: this.year, - especificacaoDoCalculoDaMedia: this.especificacaoDoCalculoDaMedia.toJSON(), + metas: this.metas, + metasLocked: this.metasLocked, // Persist the state + // Check if toJSON exists (it might be a plain object if loaded incorrectly previously) + especificacaoDoCalculoDaMedia: this.especificacaoDoCalculoDaMedia.toJSON ? this.especificacaoDoCalculoDaMedia.toJSON() : this.especificacaoDoCalculoDaMedia, enrollments: this.enrollments.map(enrollment => enrollment.toJSON()) }; } // Create Class from JSON object - static fromJSON(data: { topic: string; semester: number; year: number; especificacaoDoCalculoDaMedia: any, enrollments: any[] }, allStudents: Student[]): Class { - const enrollments = data.enrollments - ? data.enrollments.map((enrollmentData: any) => { - const student = allStudents.find(s => s.getCPF() === enrollmentData.student.cpf); - if (!student) { - throw new Error(`Student with CPF ${enrollmentData.student.cpf} not found`); - } - return Enrollment.fromJSON(enrollmentData, student); - }) - : []; + static fromJSON(data: { + topic: string; + semester: number; + year: number; + metas: string[]; + especificacaoDoCalculoDaMedia: any, + enrollments: any[], + metasLocked?: boolean // Add type definition here + }, allStudents: Student[]): Class { + const enrollments = data.enrollments.map(e => { + const studentCpf = e.student ? (e.student.cpf || e.student._cpf) : null; + const student = allStudents.find(s => s.getCPF() === studentCpf); + + if (!student) { + // If student is not found (data inconsistency), we throw an error + throw new Error(`Student with CPF ${studentCpf} not found for enrollment in class ${data.topic}`); + } + + return Enrollment.fromJSON(e, student); + }); - // Novo carregamento do EspecificacaoDoCalculoDaMedia - const especificacaoDoCalculoDaMedia = EspecificacaoDoCalculoDaMedia.fromJSON(data.especificacaoDoCalculoDaMedia); - - return new Class(data.topic, data.semester, data.year, especificacaoDoCalculoDaMedia, enrollments); + const isLocked = data.metasLocked !== undefined + ? data.metasLocked + : (data.metas && data.metas.length > 0); + + return new Class( + data.topic, + data.semester, + data.year, + data.metas, + data.especificacaoDoCalculoDaMedia, + enrollments, + isLocked + ); } } diff --git a/server/src/models/Classes.ts b/server/src/models/Classes.ts index b51640ed..4d64eeb1 100644 --- a/server/src/models/Classes.ts +++ b/server/src/models/Classes.ts @@ -119,7 +119,24 @@ export class Classes { } }); }); + return students; } + + getClassMetas(id: string): any[] { + const classObj = this.findClassById(id); + if (!classObj) { + throw new Error('Turma não encontrada!'); + } + return classObj.getMetas(); + } + + addClassMetas(id: string, metas: any[]): void { + const classObj = this.findClassById(id); + if (!classObj) { + throw new Error('Turma não encontrada!'); + } + classObj.setMetas(metas); + } } \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index 8062087f..9c8b676e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -48,6 +48,8 @@ const saveDataToFile = (): void => { topic: classObj.getTopic(), semester: classObj.getSemester(), year: classObj.getYear(), + metas: classObj.getMetas(), + metasLocked: classObj.isMetasLocked(), especificacaoDoCalculoDaMedia: classObj.getEspecificacaoDoCalculoDaMedia().toJSON(), enrollments: classObj.getEnrollments().map(enrollment => ({ studentCPF: enrollment.getStudent().getCPF(), @@ -92,7 +94,7 @@ const loadDataFromFile = (): void => { if (data.classes && Array.isArray(data.classes)) { data.classes.forEach((classData: any) => { try { - const classObj = new Class(classData.topic, classData.semester, classData.year, EspecificacaoDoCalculoDaMedia.fromJSON(classData.especificacaoDoCalculoDaMedia)); + const classObj = new Class(classData.topic, classData.semester, classData.year, classData.metas, EspecificacaoDoCalculoDaMedia.fromJSON(classData.especificacaoDoCalculoDaMedia)); classes.addClass(classObj); // Load enrollments for this class @@ -301,7 +303,7 @@ app.post('/api/classes', (req: Request, res: Response) => { return res.status(400).json({ error: 'Topic, semester, and year are required' }); } - const classObj = new Class(topic, semester, year, DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA); + const classObj = new Class(topic, semester, year, [], DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA); const newClass = classes.addClass(classObj); triggerSave(); // Save to file after adding class res.status(201).json(newClass.toJSON()); @@ -354,6 +356,46 @@ app.delete('/api/classes/:id', (req: Request, res: Response) => { } }); +// GET /api/classes/:id/metas - Pegar todas as metas de uma turma +app.get('/api/classes/:id/metas', (req: Request, res: Response) => { + try { + const { id } = req.params; + try { + const metas = classes.getClassMetas(id); + res.json({ metas }); + } catch (error) { + if ((error as Error).message === 'Turma não encontrada!') { + return res.status(404).json({ error: 'Turma não encontrada!' }); + } + throw error; + } + } catch (error) { + res.status(400).json({ error: (error as Error).message }); + } +}); + +// POST /api/classes/:id/metas - Adicionar as metas de uma turma +app.post('/api/classes/:id/metas', (req: Request, res: Response) => { + try { + const { id } = req.params; + const { metas } = req.body; + + try { + classes.addClassMetas(id, metas); + triggerSave(); // Save to file after adding metas + res.status(201).json({ message: 'Metas criadas com sucesso!' }); + } catch (err) { + const errorMessage = (err as Error).message; + if (errorMessage === 'Turma não encontrada!') { + return res.status(404).json({ error: errorMessage }); + } + return res.status(409).json({ error: errorMessage }); + } + } catch (error) { + res.status(400).json({ error: (error as Error).message }); + } +}); + // POST /api/classes/:classId/enroll - Enroll a student in a class app.post('/api/classes/:classId/enroll', (req: Request, res: Response) => { try {