diff --git a/.env.template b/.env.template index 04e93cbd..38986c0c 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,5 @@ NEXT_TELEMETRY_DISABLED=1 NEXT_PUBLIC_DEBUG=true +# if link change dont forget to chage the test for sideNav to check if the link is correct +NEXT_PUBLIC_REPORT_ISSUE_URL="https://forms.office.com/Pages/ResponsePage.aspx?id=7aW1GIYd00GUoLwn2uMqsn9SKTgKSYtCg4t0B9x4uyJURE5HSkFCTkZHUEQyWkxJVElMODdFQ09HUCQlQCN0PWcu&r5a19e9d47d9f4ac497fb974c192da4b3=%22Fertiscan%22" +NEXT_PUBLIC_ALERT_BANNER_AUTO_DISMISS_TIME=5000 diff --git a/package-lock.json b/package-lock.json index e5f4e5aa..ca92c306 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.5", "@mui/icons-material": "^6.1.4", - "@mui/material": "^6.1.4", + "@mui/material": "^6.1.8", "@mui/material-nextjs": "^6.1.4", "dotenv": "^16.4.5", "i18next": "^23.16.5", @@ -33,7 +33,7 @@ "devDependencies": { "@jest/types": "^29.6.3", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.14", @@ -41,7 +41,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "eslint": "^8", - "eslint-config-next": "14.2.15", + "eslint-config-next": "15.0.3", "jest": "^29.7.0", "postcss": "^8", "prettier": "^3.3.3", @@ -660,10 +660,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", - "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", - "license": "MIT", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1560,10 +1559,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.4.tgz", - "integrity": "sha512-jCRsB9NDJJatVCHvwWSTfYUzuTQ7E0Km6tAQWz2Md1SLHIbVj5visC9yHbf/Cv2IDcG6XdHRv3e7Bt1rIburNw==", - "license": "MIT", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.8.tgz", + "integrity": "sha512-TGAvzwUg9hybDacwfIGFjI2bXYXrIqky+vMfaeay8rvT56/PNAlvIDUJ54kpT5KRc9AWAihOvtDI7/LJOThOmQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1596,16 +1594,15 @@ } }, "node_modules/@mui/material": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.4.tgz", - "integrity": "sha512-mIVdjzDYU4U/XYzf8pPEz3zDZFS4Wbyr0cjfgeGiT/s60EvtEresXXQy8XUA0bpJDJjgic1Hl5AIRcqWDyi2eg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/core-downloads-tracker": "^6.1.4", - "@mui/system": "^6.1.4", - "@mui/types": "^7.2.18", - "@mui/utils": "^6.1.4", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.8.tgz", + "integrity": "sha512-QZdQFnXct+7NXIzHgT3qt+sQiO7HYGZU2vymP9Xl9tUMXEOA/S1mZMMb7+WGZrk5TzNlU/kP/85K0da5V1jXoQ==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.1.8", + "@mui/system": "^6.1.8", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.8", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -1624,7 +1621,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.4", + "@mui/material-pigment-css": "^6.1.8", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1686,13 +1683,12 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.4.tgz", - "integrity": "sha512-FPa+W5BSrRM/1QI5Gf/GwJinJ2WsrKPpJB6xMmmXMXSUIp31YioIVT04i28DQUXFFB3yZY12ukcZi51iLvPljw==", - "license": "MIT", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.8.tgz", + "integrity": "sha512-TuKl7msynCNCVvhX3c0ef1sF0Qb3VHcPs8XOGB/8bdOGBr/ynmIG1yTMjZeiFQXk8yN9fzK/FDEKMFxILNn3wg==", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^6.1.4", + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.1.8", "prop-types": "^15.8.1" }, "engines": { @@ -1713,12 +1709,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.4.tgz", - "integrity": "sha512-D+aiIDtJsU9OVJ7dgayhCDABJHT7jTlnz1FKyxa5mNVHsxjjeG1M4OpLsRQvx4dcvJfDywnU2cE+nFm4Ln2aFQ==", - "license": "MIT", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.8.tgz", + "integrity": "sha512-ZvEoT0U2nPLSLI+B4by4cVjaZnPT2f20f4JUPkyHdwLv65ZzuoHiTlwyhqX1Ch63p8bcJzKTHQVGisEoMK6PGA==", "dependencies": { - "@babel/runtime": "^7.25.7", + "@babel/runtime": "^7.26.0", "@emotion/cache": "^11.13.1", "@emotion/serialize": "^1.3.2", "@emotion/sheet": "^1.4.0", @@ -1747,16 +1742,15 @@ } }, "node_modules/@mui/system": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.4.tgz", - "integrity": "sha512-lCveY/UtDhYwMg1WnLc3wEEuGymLi6YI79VOwFV9zfZT5Et+XEw/e1It26fiKwUZ+mB1+v1iTYMpJnwnsrn2aQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/private-theming": "^6.1.4", - "@mui/styled-engine": "^6.1.4", - "@mui/types": "^7.2.18", - "@mui/utils": "^6.1.4", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.8.tgz", + "integrity": "sha512-i1kLfQoWxzFpXTBQIuPoA3xKnAnP3en4I2T8xIolovSolGQX5k8vGjw1JaydQS40td++cFsgCdEU458HDNTGUA==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.1.8", + "@mui/styled-engine": "^6.1.8", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.8", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1787,10 +1781,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.18", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", - "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", - "license": "MIT", + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1801,13 +1794,12 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.4.tgz", - "integrity": "sha512-v0wXkyh3/Hpw48ivlNvgs4ZT6M8BIEAMdLgvct59rQBggYFhoAVKyliKDzdj37CnIlYau3DYIn7x5bHlRYFBow==", - "license": "MIT", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.8.tgz", + "integrity": "sha512-O2DWb1kz8hiANVcR7Z4gOB3SvPPsSQGUmStpyBDzde6dJIfBzgV9PbEQOBZd3EBsd1pB+Uv1z5LAJAbymmawrA==", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/types": "^7.2.18", + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.19", "@types/prop-types": "^15.7.13", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1833,8 +1825,7 @@ "node_modules/@mui/utils/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/@next/env": { "version": "14.2.15", @@ -1843,13 +1834,160 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.15.tgz", - "integrity": "sha512-pKU0iqKRBlFB/ocOI1Ip2CkKePZpYpnw5bEItEkuZ/Nr9FQP1+p7VDWr4VfOdff4i9bFmrOaeaU1bFEyAcxiMQ==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.3.tgz", + "integrity": "sha512-3Ln/nHq2V+v8uIaxCR6YfYo7ceRgZNXfTd3yW1ukTaFbO+/I8jNakrjYWODvG9BuR2v5kgVtH/C8r0i11quOgw==", "dev": true, - "license": "MIT", "dependencies": { - "glob": "10.3.10" + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", + "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", + "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", + "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", + "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", + "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", + "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", + "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", + "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@next/swc-darwin-arm64": { @@ -2142,11 +2280,10 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", - "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -3572,7 +3709,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -4458,25 +4594,24 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.15.tgz", - "integrity": "sha512-mKg+NC/8a4JKLZRIOBplxXNdStgxy7lzWuedUaCc8tev+Al9mwDUTujQH6W6qXDH9kycWiVo28tADWGvpBsZcQ==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.3.tgz", + "integrity": "sha512-IGP2DdQQrgjcr4mwFPve4DrCqo7CVVez1WoYY47XwKSrYO4hC0Dlb+iJA60i0YfICOzgNADIb8r28BpQ5Zs0wg==", "dev": true, - "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.15", - "@rushstack/eslint-patch": "^1.3.3", + "@next/eslint-plugin-next": "15.0.3", + "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -4703,16 +4838,15 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0-canary-7118f5dd7-20230705", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", - "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -8336,9 +8470,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -8354,10 +8488,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { diff --git a/package.json b/package.json index 0fa9668e..1dbe5368 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.5", "@mui/icons-material": "^6.1.4", - "@mui/material": "^6.1.4", + "@mui/material": "^6.1.8", "@mui/material-nextjs": "^6.1.4", "dotenv": "^16.4.5", "i18next": "^23.16.5", @@ -47,7 +47,7 @@ "devDependencies": { "@jest/types": "^29.6.3", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.14", @@ -55,7 +55,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "eslint": "^8", - "eslint-config-next": "14.2.15", + "eslint-config-next": "15.0.3", "jest": "^29.7.0", "postcss": "^8", "prettier": "^3.3.3", diff --git a/public/locales/en/labelDataValidationPage.json b/public/locales/en/labelDataValidationPage.json new file mode 100644 index 00000000..c545a1d1 --- /dev/null +++ b/public/locales/en/labelDataValidationPage.json @@ -0,0 +1,103 @@ +{ + "baseInformation": { + "stepTitle": "Base Information", + "fields": { + "name": { + "label": "Name", + "placeholder": "Enter name" + }, + "registrationNumber": { + "label": "Registration Number", + "placeholder": "Enter registration number" + }, + "lotNumber": { + "label": "Lot Number", + "placeholder": "Enter lot number" + }, + "npk": { + "label": "NPK", + "placeholder": "Enter NPK" + }, + "weight": { + "label": "Weight" + }, + "density": { + "label": "Density" + }, + "volume": { + "label": "Volume" + } + } + }, + "organizations": { + "stepTitle": "Organizations" + }, + "cautions": { + "stepTitle": "Cautions" + }, + "instructions": { + "stepTitle": "Instructions" + }, + "verifiedQuantityMultiInput": { + "deleteRow": "Delete this row", + "defaultPlaceholder": "Enter value", + "addRow": "Add Row", + "removeRow": "Remove", + "verify": "Mark as Verified", + "unverify": "Mark as Unverified", + "errors": { + "numbersOnly": "Numbers only", + "minValue": "Minimum value is 0", + "duplicateUnit": "Duplicate unit" + }, + "units": { + "kg": "kg", + "g": "g", + "lb": "lb", + "tonne": "tonne", + "L": "L", + "mL": "mL", + "gal": "gal", + "ft³": "ft³", + "lb/ft³": "lb/ft³", + "g/cm³": "g/cm³", + "kg/m³": "kg/m³", + "lb/gal": "lb/gal" + }, + "accessibility": { + "deleteRowButton": "Button to delete this row", + "addRowButton": "Button to add a new row", + "valueInput": "Input field for numeric value", + "unitDropdown": "Dropdown to select a unit of measurement", + "verifyToggleButton": "Button to toggle verification status", + "fieldsContainer": "Container for all input fields", + "row": "Row containing inputs for unit and value", + "errorIcon": "Icon indicating a validation error" + } + }, + "verifiedBilingualTable": { + "english": "English", + "french": "French", + "actions": "Actions", + "placeholders": { + "english": "Enter English text", + "french": "Enter French text" + }, + "addRow": "Add Row", + "delete": "Delete Row", + "verify": "Mark as Verified", + "unverify": "Mark as Unverified", + "verifyAll": "Mark All as Verified", + "unverifyAll": "Mark All as Unverified", + "accessibility": { + "englishInput": "English text input for row {{row}}", + "frenchInput": "French text input for row {{row}}", + "deleteButton": "Button to delete this row", + "addRowButton": "Button to add a new row", + "verifyButton": "Button to mark this row as Verified", + "unverifyButton": "Button to mark this row as Unverified", + "verifyAllButton": "Button to mark all rows as Verified", + "unverifyAllButton": "Button to mark all rows as Unverified" + } + } +} diff --git a/public/locales/fr/labelDataValidationPage.json b/public/locales/fr/labelDataValidationPage.json new file mode 100644 index 00000000..6eaf6ff9 --- /dev/null +++ b/public/locales/fr/labelDataValidationPage.json @@ -0,0 +1,103 @@ +{ + "baseInformation": { + "stepTitle": "Informations de base sur l'engrais", + "fields": { + "name": { + "label": "Nom", + "placeholder": "Entrez le nom" + }, + "registrationNumber": { + "label": "Numéro d'enregistrement", + "placeholder": "Entrez le numéro d'enregistrement" + }, + "lotNumber": { + "label": "Numéro de lot", + "placeholder": "Entrez le numéro de lot" + }, + "npk": { + "label": "NPK", + "placeholder": "Entrez le NPK" + }, + "weight": { + "label": "Poids" + }, + "density": { + "label": "Densité" + }, + "volume": { + "label": "Volume" + } + } + }, + "organizations": { + "stepTitle": "Organisations" + }, + "cautions": { + "stepTitle": "Mises en garde" + }, + "instructions": { + "stepTitle": "Instructions" + }, + "verifiedQuantityMultiInput": { + "deleteRow": "Supprimer cette ligne", + "defaultPlaceholder": "Entrez une valeur", + "addRow": "Ajouter une ligne", + "removeRow": "Supprimer", + "verify": "Marquer comme vérifiés", + "unverify": "Marquer comme non vérifiés", + "errors": { + "numbersOnly": "Uniquement des nombres", + "minValue": "La valeur minimale est 0", + "duplicateUnit": "Unité dupliquée" + }, + "units": { + "kg": "kg", + "g": "g", + "lb": "lb", + "tonne": "tonne", + "L": "L", + "mL": "mL", + "gal": "gal", + "ft³": "pi³", + "lb/ft³": "lb/pi³", + "g/cm³": "g/cm³", + "kg/m³": "kg/m³", + "lb/gal": "lb/gal" + }, + "accessibility": { + "deleteRowButton": "Bouton pour supprimer cette ligne", + "addRowButton": "Bouton pour ajouter une nouvelle ligne", + "valueInput": "Champ de saisie pour une valeur numérique", + "unitDropdown": "Menu déroulant pour sélectionner une unité de mesure", + "verifyToggleButton": "Bouton pour basculer l'état de vérification", + "fieldsContainer": "Conteneur pour tous les champs de saisie", + "row": "Ligne contenant les champs pour l'unité et la valeur", + "errorIcon": "Icône indiquant une erreur de validation" + } + }, + "verifiedBilingualTable": { + "english": "Anglais", + "french": "Français", + "actions": "Actions", + "placeholders": { + "english": "Entrez le texte en anglais", + "french": "Entrez le texte en français" + }, + "addRow": "Ajouter une ligne", + "delete": "Supprimer cette ligne", + "verify": "Marquer comme vérifié", + "unverify": "Marquer comme non vérifié", + "verifyAll": "Tout marquer comme vérifié", + "unverifyAll": "Tout marquer comme non vérifié", + "accessibility": { + "englishInput": "Zone de texte en anglais pour la ligne {{row}}", + "frenchInput": "Zone de texte en français pour la ligne {{row}}", + "deleteButton": "Bouton pour supprimer cette ligne", + "addRowButton": "Bouton pour ajouter une nouvelle ligne", + "verifyButton": "Bouton pour marquer cette ligne comme vérifiée", + "unverifyButton": "Bouton pour marquer cette ligne comme non vérifiée", + "verifyAllButton": "Bouton pour tout marquer comme vérifié", + "unverifyAllButton": "Bouton pour tout marquer comme non vérifié" + } + } +} diff --git a/src/app/label-data-validation/__tests__/page.test.tsx b/src/app/label-data-validation/__tests__/page.test.tsx index e4d30bcc..c37a1c91 100644 --- a/src/app/label-data-validation/__tests__/page.test.tsx +++ b/src/app/label-data-validation/__tests__/page.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { act, fireEvent, render, screen } from "@testing-library/react"; import LabelDataValidationPage from "../page"; jest.mock("@/components/ImageViewer", () => ({ @@ -20,8 +20,8 @@ describe("LabelDataValidationPage Rendering", () => { it("renders the correct step component initially", () => { render(); - expect(screen.getByTestId("organizations-form")).toBeInTheDocument(); - expect(screen.queryByTestId("Dummy Step")).not.toBeInTheDocument(); + expect(screen.getByTestId("base-information-form")).toBeInTheDocument(); + expect(screen.queryByTestId("organizations-form")).not.toBeInTheDocument(); }); }); @@ -32,14 +32,16 @@ describe("LabelDataValidationPage Functionality", () => { const nextButton = screen.getByText("Next"); fireEvent.click(nextButton); - expect(screen.queryByTestId("organizations-form")).not.toBeInTheDocument(); - expect(screen.getByTestId("Dummy Step")).toBeInTheDocument(); + expect( + screen.queryByTestId("base-information-form"), + ).not.toBeInTheDocument(); + expect(screen.getByTestId("organizations-form")).toBeInTheDocument(); const backButton = screen.getByText("Back"); fireEvent.click(backButton); - expect(screen.getByTestId("organizations-form")).toBeInTheDocument(); - expect(screen.queryByTestId("Dummy Step")).not.toBeInTheDocument(); + expect(screen.getByTestId("base-information-form")).toBeInTheDocument(); + expect(screen.queryByTestId("organizations-form")).not.toBeInTheDocument(); }); it("does not navigate beyond the first or last step", () => { @@ -49,11 +51,12 @@ describe("LabelDataValidationPage Functionality", () => { const backButton = screen.getByText("Back"); fireEvent.click(backButton); - expect(screen.getByTestId("organizations-form")).toBeInTheDocument(); + expect(screen.getByTestId("base-information-form")).toBeInTheDocument(); fireEvent.click(nextButton); fireEvent.click(nextButton); - expect(screen.getByTestId("Dummy Step")).toBeInTheDocument(); + fireEvent.click(nextButton); + expect(screen.getByTestId("instructions-form")).toBeInTheDocument(); }); it("renders the mocked Image Viewer", () => { @@ -64,11 +67,13 @@ describe("LabelDataValidationPage Functionality", () => { }); }); -describe("LabelDataValidationPage and OrganizationsForm Integration", () => { - it("marks the Organizations step as Completed when all organizations are Verified", () => { +describe("LabelDataValidationPage and Forms Integration", () => { + it("marks the Organizations step as Completed or Incomplete when fields are Verified", () => { render(); - const spans = screen.getAllByText("Organizations", { exact: true }); + const spans = screen.getAllByText("organizations.stepTitle", { + exact: true, + }); const targetSpan = spans.find((span) => span.classList.contains("MuiStepLabel-label"), ); @@ -79,28 +84,116 @@ describe("LabelDataValidationPage and OrganizationsForm Integration", () => { const verifyAllButton = screen.getByTestId("verify-all-btn-0"); fireEvent.click(verifyAllButton); expect(targetSpan).toHaveClass("Mui-completed"); + + fireEvent.click( + screen.getByTestId("verified-icon-organizations.0.address.verified"), + ); + + expect(targetSpan).not.toHaveClass("Mui-completed"); }); - it("keeps the Organizations step as Incomplete when at least one organization is not Verified", () => { + it("marks the Base Information step as Completed or Incomplete when fields are Verified", async () => { render(); - const spans = screen.getAllByText("Organizations", { exact: true }); + const spans = screen.getAllByText("baseInformation.stepTitle", { + exact: true, + }); const targetSpan = spans.find((span) => span.classList.contains("MuiStepLabel-label"), ); expect(targetSpan).not.toHaveClass("Mui-completed"); const button = targetSpan!.closest("button"); - fireEvent.click(button!); + await act(async () => { + fireEvent.click(button!); + }); + + const verifyButtons = screen.getAllByTestId( + /verified-icon-baseInformation/, + ); + expect(verifyButtons.length).toBeGreaterThanOrEqual(7); + + for (const button of verifyButtons) { + await act(async () => { + fireEvent.click(button); + }); + } + + expect(targetSpan).toHaveClass("Mui-completed"); + + await act(async () => { + fireEvent.click(verifyButtons[0]); + }); + + expect(targetSpan).not.toHaveClass("Mui-completed"); + }); + + it("marks the Cautions step as Completed or Incomplete when fields are Verified", async () => { + render(); + + const spans = screen.getAllByText("cautions.stepTitle", { + exact: true, + }); + const targetSpan = spans.find((span) => + span.classList.contains("MuiStepLabel-label"), + ); + expect(targetSpan).not.toHaveClass("Mui-completed"); + + const button = targetSpan!.closest("button"); + await act(async () => { + fireEvent.click(button!); + }); + + const verifyButtons = screen.getAllByTestId(/verify-row-btn-cautions-\d+/); + expect(verifyButtons.length).toBeGreaterThanOrEqual(1); + + for (const button of verifyButtons) { + await act(async () => { + fireEvent.click(button); + }); + } - const verifyAllButton = screen.getByTestId("verify-all-btn-0"); - fireEvent.click(verifyAllButton); expect(targetSpan).toHaveClass("Mui-completed"); - const toggleStatusButton = screen.getByTestId( - "toggle-status-btn-organizations.0.name.status", + await act(async () => { + fireEvent.click(verifyButtons[0]); + }); + + expect(targetSpan).not.toHaveClass("Mui-completed"); + }); + + it("marks the Instructions step as Completed or Incomplete when fields are Verified", async () => { + render(); + + const spans = screen.getAllByText("instructions.stepTitle", { + exact: true, + }); + const targetSpan = spans.find((span) => + span.classList.contains("MuiStepLabel-label"), ); - fireEvent.click(toggleStatusButton); + expect(targetSpan).not.toHaveClass("Mui-completed"); + + const button = targetSpan!.closest("button"); + await act(async () => { + fireEvent.click(button!); + }); + + const verifyButtons = screen.getAllByTestId( + /verify-row-btn-instructions-\d+/, + ); + expect(verifyButtons.length).toBeGreaterThanOrEqual(1); + + for (const button of verifyButtons) { + await act(async () => { + fireEvent.click(button); + }); + } + + expect(targetSpan).toHaveClass("Mui-completed"); + + await act(async () => { + fireEvent.click(verifyButtons[0]); + }); expect(targetSpan).not.toHaveClass("Mui-completed"); }); diff --git a/src/app/label-data-validation/page.tsx b/src/app/label-data-validation/page.tsx index 52a9d4d3..7e4af082 100644 --- a/src/app/label-data-validation/page.tsx +++ b/src/app/label-data-validation/page.tsx @@ -1,24 +1,28 @@ "use client"; -import DummyStepComponent from "@/components/DummyStepComponent"; +import BaseInformationForm from "@/components/BaseInformationForm"; +import CautionsForm from "@/components/CautionsForm"; import ImageViewer from "@/components/ImageViewer"; +import InstructionsForm from "@/components/InstructionsForm"; import OrganizationsForm from "@/components/OrganizationsForm"; import { HorizontalNonLinearStepper, StepperControls, StepStatus, } from "@/components/stepper"; +import useAlertStore from "@/stores/alertStore"; import { - checkOrganizationStatus, DEFAULT_LABEL_DATA, - FieldStatus, FormComponentProps, + isVerified, LabelData, } from "@/types/types"; import useBreakpoints from "@/utils/useBreakpoints"; -import { Box, Button, Container } from "@mui/material"; +import { Box, Button, Container, Typography } from "@mui/material"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; function LabelDataValidationPage() { + const { t } = useTranslation("labelDataValidationPage"); const [imageFiles, setImageFiles] = useState([]); const { isDownXs, isBetweenXsSm, isBetweenSmMd, isBetweenMdLg } = useBreakpoints(); @@ -28,9 +32,14 @@ function LabelDataValidationPage() { const [activeStep, setActiveStep] = useState(0); const [organizationsStepStatus, setOrganizationsStepStatus] = useState(StepStatus.Incomplete); - const [dummyStepStatus, setDummyStepStatus] = useState( + const [baseInformationStepStatus, setBaseInformationStepStatus] = + useState(StepStatus.Incomplete); + const [cautionsStepStatus, setCautionsStepStatus] = useState( StepStatus.Incomplete, ); + const [instructionsStepStatus, setInstructionsStepStatus] = + useState(StepStatus.Incomplete); + const { showAlert } = useAlertStore(); const createStep = ( title: string, @@ -43,27 +52,35 @@ function LabelDataValidationPage() { stepStatus: stepStatus, setStepStatus: setStepStatusState, render: () => ( - + ), }; }; const steps = [ createStep( - "Organizations", + t("baseInformation.stepTitle"), + BaseInformationForm, + baseInformationStepStatus, + setBaseInformationStepStatus, + ), + createStep( + t("organizations.stepTitle"), OrganizationsForm, organizationsStepStatus, setOrganizationsStepStatus, ), createStep( - "Dummy Step", - DummyStepComponent, - dummyStepStatus, - setDummyStepStatus, + t("cautions.stepTitle"), + CautionsForm, + cautionsStepStatus, + setCautionsStepStatus, + ), + createStep( + t("instructions.stepTitle"), + InstructionsForm, + instructionsStepStatus, + setInstructionsStepStatus, ), ]; @@ -78,17 +95,38 @@ function LabelDataValidationPage() { }; useEffect(() => { - const verified = labelData.organizations.every((org) => - checkOrganizationStatus(org, FieldStatus.Verified), - ); + const verified = labelData.organizations.every((org) => isVerified(org)); setOrganizationsStepStatus( verified ? StepStatus.Completed : StepStatus.Incomplete, ); }, [labelData.organizations, setOrganizationsStepStatus]); + useEffect(() => { + const verified = isVerified(labelData.baseInformation); + setBaseInformationStepStatus( + verified ? StepStatus.Completed : StepStatus.Incomplete, + ); + }, [labelData.baseInformation, setBaseInformationStepStatus]); + + useEffect(() => { + const verified = labelData.cautions.every((caution) => caution.verified); + setCautionsStepStatus( + verified ? StepStatus.Completed : StepStatus.Incomplete, + ); + }, [labelData.cautions, setCautionsStepStatus]); + + useEffect(() => { + const verified = labelData.instructions.every( + (instruction) => instruction.verified, + ); + setInstructionsStepStatus( + verified ? StepStatus.Completed : StepStatus.Incomplete, + ); + }, [labelData.instructions, setInstructionsStepStatus]); + return ( @@ -104,12 +142,12 @@ function LabelDataValidationPage() { )} @@ -126,18 +164,26 @@ function LabelDataValidationPage() { )} - - {steps[activeStep].render()} - step.title)} - stepStatuses={steps.map((step) => step.stepStatus)} - activeStep={activeStep} - setActiveStep={setActiveStep} - /> + + {steps[activeStep].title} + + {/* */} + + {steps[activeStep].render()} + step.title)} + stepStatuses={steps.map((step) => step.stepStatus)} + activeStep={activeStep} + setActiveStep={setActiveStep} + /> @@ -153,6 +199,7 @@ function LabelDataValidationPage() { style={{ display: "none" }} onChange={handleFileChange} /> + ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dd1bb27f..181d0db6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,15 +1,16 @@ "use client"; import Header from "@/components/Header"; import SideNav from "@/components/Sidenav"; +import useAlertStore from "@/stores/alertStore"; +import { Box } from "@mui/material"; import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; import { ThemeProvider } from "@mui/material/styles"; +import "dotenv/config"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import "./globals.css"; -import theme from "./theme"; -import "dotenv/config"; import "./i18n"; -import useAlertStore from "@/stores/alertStore"; -import { useTranslation } from "react-i18next"; +import theme from "./theme"; export default function RootLayout({ children, @@ -17,10 +18,10 @@ export default function RootLayout({ const [sideNavOpen, setSideNavOpen] = useState(false); const { showAlert } = useAlertStore(); const { t, i18n } = useTranslation(["alertBanner", "translation"]); - const debugMode = process.env.NEXT_PUBLIC_DEBUG === 'true'; + const debugMode = process.env.NEXT_PUBLIC_DEBUG === "true"; if (debugMode) { - console.log(t("debugMessage")); + console.log(t("debugMessage")); } const handleDrawerClose = () => { @@ -40,7 +41,7 @@ export default function RootLayout({
- {children} + {children} diff --git a/src/app/theme.ts b/src/app/theme.ts index 773eed76..e43b871a 100644 --- a/src/app/theme.ts +++ b/src/app/theme.ts @@ -87,7 +87,6 @@ const theme = createTheme({ root: { "&.header": { backgroundColor: "#05486C", // Dark Blue background - height: "64px", // Height of the AppBar }, }, }, diff --git a/src/components/AlertBanner.tsx b/src/components/AlertBanner.tsx index 8c0cd7d4..2083387c 100644 --- a/src/components/AlertBanner.tsx +++ b/src/components/AlertBanner.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useRef } from "react"; import useAlertStore from "../stores/alertStore"; const AUTO_DISMISS_TIME = - Number(process.env.NEXT_PUBLIC_AUTO_DISMISS_TIME) || 5000; + Number(process.env.NEXT_PUBLIC_ALERT_BANNER_AUTO_DISMISS_TIME) || 5000; const AlertBanner: React.FC = () => { const { alert, hideAlert } = useAlertStore(); @@ -36,15 +36,20 @@ const AlertBanner: React.FC = () => { }, [alert, hideAlert, startAutoDismissTimer]); return ( - + {alert && ( + } @@ -53,6 +58,7 @@ const AlertBanner: React.FC = () => { className="overflow-hidden text-ellipsis" variant="body2" color="inherit" + data-testid="alert-message" style={{ display: "-webkit-box", WebkitLineClamp: 2, diff --git a/src/components/BaseInformationForm.tsx b/src/components/BaseInformationForm.tsx new file mode 100644 index 00000000..9051c802 --- /dev/null +++ b/src/components/BaseInformationForm.tsx @@ -0,0 +1,78 @@ +import { FormComponentProps, LabelData, UNITS } from "@/types/types"; +import { Box } from "@mui/material"; +import { useEffect } from "react"; +import { FormProvider, useForm, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import VerifiedInput from "./VerifiedInput"; +import VerifiedQuantityMultiInput from "./VerifiedQuantityMultiInput"; + +function BaseInformationForm({ labelData, setLabelData }: FormComponentProps) { + const methods = useForm({ + defaultValues: labelData, + }); + + const { t } = useTranslation("labelDataValidationPage"); + const { control } = methods; + + const watchedBaseInformation = useWatch({ + control, + name: "baseInformation", + }); + + useEffect(() => { + if (watchedBaseInformation) { + setLabelData((prevLabelData) => ({ + ...prevLabelData, + baseInformation: watchedBaseInformation, + })); + } + }, [watchedBaseInformation, setLabelData]); + + return ( + + + + + + + + + + + + + + ); +} + +export default BaseInformationForm; diff --git a/src/components/CautionsForm.tsx b/src/components/CautionsForm.tsx new file mode 100644 index 00000000..0d67b863 --- /dev/null +++ b/src/components/CautionsForm.tsx @@ -0,0 +1,37 @@ +import { FormComponentProps, LabelData } from "@/types/types"; +import { Box } from "@mui/material"; +import { useEffect } from "react"; +import { FormProvider, useForm, useWatch } from "react-hook-form"; +import VerifiedBilingualTable from "./VerifiedBilingualTable"; + +function CautionsForm({ labelData, setLabelData }: FormComponentProps) { + const methods = useForm({ + defaultValues: labelData, + }); + + const { control } = methods; + + const watchedCautions = useWatch({ + control, + name: "cautions", + }); + + useEffect(() => { + if (watchedCautions) { + setLabelData((prevLabelData) => ({ + ...prevLabelData, + cautions: watchedCautions, + })); + } + }, [watchedCautions, setLabelData]); + + return ( + + + + + + ); +} + +export default CautionsForm; diff --git a/src/components/DummyStepComponent.tsx b/src/components/DummyStepComponent.tsx deleted file mode 100644 index 0f7de829..00000000 --- a/src/components/DummyStepComponent.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { FormComponentProps } from "@/types/types"; - -function DummyStepComponent({ title }: FormComponentProps) { - return
{title}
; -} - -export default DummyStepComponent; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 3ff4829a..386a248e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -12,12 +12,12 @@ import { Typography, useTheme, } from "@mui/material"; +import i18next from "i18next"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; -import AlertBanner from "./AlertBanner"; -import i18next from "i18next"; import { useTranslation } from "react-i18next"; +import AlertBanner from "./AlertBanner"; interface HeaderProps { setSideNavOpen: (open: boolean | ((prevOpen: boolean) => boolean)) => void; @@ -60,90 +60,85 @@ const Header: React.FC = ({ setSideNavOpen }) => { }; return ( - <> - - + + {/* Navigation menu toggle button on the left */} + - - {/* Navigation menu toggle button on the left */} - - - + + + + {/* Logo container in the center */} + + + {t("altText.logoCFIAAlt")} + + - {/* Logo container in the center */} - + {/* Language toggle button */} + - {/* User interaction components on the right */} - - {/* Language toggle button */} - + {/* User account icon button */} + + + + + - {/* User account icon button */} - - - - - - + {/* User menu */} + - -
- - + + ); }; diff --git a/src/components/ImageViewer.tsx b/src/components/ImageViewer.tsx index bd8b9049..9407a279 100644 --- a/src/components/ImageViewer.tsx +++ b/src/components/ImageViewer.tsx @@ -51,7 +51,7 @@ const ImageViewer: React.FC = ({ imageFiles }) => { return ( = ({ imageFiles }) => { loop={false} speed={0} data-testid="swiper" - className="w-full h-full" + className="size-full" onSwiper={(swiper) => setSwiperInstance(swiper)} onSlideChange={(swiper) => setActiveIndex(swiper.activeIndex)} noSwiping noSwipingClass="no-swipe" - pagination={{ clickable: true }} // Enable pagination - modules={[Pagination]} // Register the Pagination module + pagination={{ clickable: true }} + modules={[Pagination]} > {imageUrls.map((url, index) => ( - + handleInit(index, ref)} @@ -90,7 +90,7 @@ const ImageViewer: React.FC = ({ imageFiles }) => { > {`Slide = ({ className="flex items-center justify-center gap-4 p-4 flex-wrap" data-testid="control-bar" > - - + + + + - - + = zoomRefs.length - 1} + > + + + - - + + + + - - + + + + - - + + + + ); diff --git a/src/components/InstructionsForm.tsx b/src/components/InstructionsForm.tsx new file mode 100644 index 00000000..ba9a2995 --- /dev/null +++ b/src/components/InstructionsForm.tsx @@ -0,0 +1,37 @@ +import { FormComponentProps, LabelData } from "@/types/types"; +import { Box } from "@mui/material"; +import { useEffect } from "react"; +import { FormProvider, useForm, useWatch } from "react-hook-form"; +import VerifiedBilingualTable from "./VerifiedBilingualTable"; + +function InstructionsForm({ labelData, setLabelData }: FormComponentProps) { + const methods = useForm({ + defaultValues: labelData, + }); + + const { control } = methods; + + const watchedInstructions = useWatch({ + control, + name: "instructions", + }); + + useEffect(() => { + if (watchedInstructions) { + setLabelData((prevLabelData) => ({ + ...prevLabelData, + instructions: watchedInstructions, + })); + } + }, [watchedInstructions, setLabelData]); + + return ( + + + + + + ); +} + +export default InstructionsForm; diff --git a/src/components/OrganizationsForm.tsx b/src/components/OrganizationsForm.tsx index abc26164..7cbf10fe 100644 --- a/src/components/OrganizationsForm.tsx +++ b/src/components/OrganizationsForm.tsx @@ -1,16 +1,15 @@ import { - checkOrganizationStatus, DEFAULT_ORGANIZATION, - FieldStatus, FormComponentProps, + isVerified, LabelData, Organization, } from "@/types/types"; import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; import DoneAllIcon from "@mui/icons-material/DoneAll"; -import RemoveIcon from "@mui/icons-material/Remove"; import RemoveDoneIcon from "@mui/icons-material/RemoveDone"; -import { Box, Button, Tooltip, Typography } from "@mui/material"; +import { Box, Button, Tooltip } from "@mui/material"; import { useCallback, useEffect } from "react"; import { FieldPath, @@ -19,14 +18,13 @@ import { useForm, useWatch, } from "react-hook-form"; -import InputWithStatus from "./InputWithStatus"; +import VerifiedInput from "./VerifiedInput"; const fieldNames = Object.keys(DEFAULT_ORGANIZATION) as Array< keyof Organization >; const OrganizationsForm: React.FC = ({ - title, labelData, setLabelData, }) => { @@ -55,12 +53,12 @@ const OrganizationsForm: React.FC = ({ } }, [watchedOrganizations, setLabelData]); - const setAllFieldsStatus = useCallback( - (orgIndex: number, status: FieldStatus) => { + const setAllVerified = useCallback( + (orgIndex: number, verified: boolean) => { fieldNames.forEach((fieldName) => { const fieldPath = - `organizations.${orgIndex}.${fieldName}.status` as FieldPath; - setValue(fieldPath, status, { + `organizations.${orgIndex}.${fieldName}.verified` as FieldPath; + setValue(fieldPath, verified, { shouldValidate: true, shouldDirty: true, }); @@ -69,26 +67,14 @@ const OrganizationsForm: React.FC = ({ [setValue], ); - const areAllFieldStatus = (index: number, status: FieldStatus) => { - const currentOrg = watchedOrganizations?.[index]; - return checkOrganizationStatus(currentOrg, status); - }; - return ( -
- - {title} - + {fields.map((field, index) => ( @@ -96,19 +82,17 @@ const OrganizationsForm: React.FC = ({ @@ -167,7 +149,7 @@ const OrganizationsForm: React.FC = ({ -
+
); }; @@ -178,32 +160,28 @@ function OrganizationInformation({ index }: { index: number }) { className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-1 xxl:grid-cols-2 gap-4" data-testid={`organization-info-${index}`} > - - - -
diff --git a/src/components/Sidenav.tsx b/src/components/Sidenav.tsx index a3cd80d2..47b3df95 100644 --- a/src/components/Sidenav.tsx +++ b/src/components/Sidenav.tsx @@ -90,8 +90,9 @@ const SideNav = ({ open, onClose }: DrawerMenuProps) => { diff --git a/src/components/VerifiedBilingualTable.tsx b/src/components/VerifiedBilingualTable.tsx new file mode 100644 index 00000000..08afd3fa --- /dev/null +++ b/src/components/VerifiedBilingualTable.tsx @@ -0,0 +1,266 @@ +import { BilingualField, DEFAULT_BILINGUAL_FIELD } from "@/types/types"; +import AddIcon from "@mui/icons-material/Add"; +import CheckIcon from "@mui/icons-material/Check"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DoneAllIcon from "@mui/icons-material/DoneAll"; +import RemoveDoneIcon from "@mui/icons-material/RemoveDone"; +import { + Box, + Button, + Divider, + IconButton, + InputBase, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@mui/material"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +const VerifiedBilingualTable = ({ path }: { path: string }) => { + const { control, setValue, getValues } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: path, + }); + const { t } = useTranslation("labelDataValidationPage"); + + const setAllVerified = (verified: boolean) => { + const rows = getValues(path).map((row: BilingualField) => ({ + ...row, + verified, + })); + setValue(path, rows); + }; + + return ( + + + + {/* Table Head */} + + + + + {t("verifiedBilingualTable.english")} + + + + + {t("verifiedBilingualTable.french")} + + + + + {t("verifiedBilingualTable.actions")} + + + + + + {/* Table Body */} + + {fields.map((field, index) => ( + + {/* English input field */} + + ( + + )} + /> + + + {/* French input field */} + + ( + + )} + /> + + + {/* Action buttons (Delete and Verify) */} + + + {/* Delete Button */} + + + remove(index)} + disabled={getValues(`${path}.${index}.verified`)} + aria-label={t( + "verifiedBilingualTable.accessibility.deleteButton", + )} + data-testid={`delete-row-btn-${path}-${index}`} + > + + + + + + + {/* Verify Button */} + ( + + + onChange(!value)} + aria-label={t( + value + ? "verifiedBilingualTable.accessibility.unverifyButton" + : "verifiedBilingualTable.accessibility.verifyButton", + )} + data-testid={`verify-row-btn-${path}-${index}`} + > + + + + )} + /> + + + + ))} + +
+
+ + {/* Buttons to add rows and bulk verify/unverify */} + + {/* Add Row Button */} + + + {/* Mark all as Verified Button */} + row.verified, + )} + > + + + + + + {/* Mark all as Unverified Button */} + !row.verified, + )} + > + + + + + +
+ ); +}; + +export default VerifiedBilingualTable; diff --git a/src/components/InputWithStatus.tsx b/src/components/VerifiedInput.tsx similarity index 53% rename from src/components/InputWithStatus.tsx rename to src/components/VerifiedInput.tsx index af655ef5..d817a396 100644 --- a/src/components/InputWithStatus.tsx +++ b/src/components/VerifiedInput.tsx @@ -1,4 +1,3 @@ -import { FieldStatus } from "@/types/types"; import CheckIcon from "@mui/icons-material/Check"; import { Box, @@ -11,55 +10,42 @@ import { import { useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -function InputWithStatus({ +function VerifiedInput({ label, placeholder, - name, - statusName, + path, className = "", }: { label: string; placeholder: string; - name: string; - statusName: string; + path: string; className?: string; }) { const { control } = useFormContext(); const [isFocused, setIsFocused] = useState(false); + const valuePath = `${path}.value`; + const verifiedPath = `${path}.verified`; - const statusValue = useWatch({ + const verified: boolean = useWatch({ control, - name: statusName, + name: verifiedPath, }); - const toggleVerified = ( - currentStatus: FieldStatus, - setStatus: (value: FieldStatus) => void, - ) => { - if (currentStatus !== FieldStatus.Error) { - setStatus( - currentStatus === FieldStatus.Verified - ? FieldStatus.Unverified - : FieldStatus.Verified, - ); - } - }; - return ( {label} ( setIsFocused(true)} - onBlur={() => setIsFocused(false)} - disabled={statusValue === FieldStatus.Verified} - data-testid={`input-field-${name}`} + onBlur={(e) => { + setIsFocused(false); + field.onChange(e.target.value.trim()); + }} + disabled={verified} + data-testid={`input-field-${valuePath}`} /> )} /> @@ -77,29 +66,23 @@ function InputWithStatus({ orientation="vertical" flexItem className={isFocused ? "!border-fertiscan-blue" : ""} - data-testid={`divider-${name}`} + data-testid={`divider-${path}`} /> ( toggleVerified(value, onChange)} - data-testid={`toggle-status-btn-${statusName}`} + onClick={() => onChange(!value)} + data-testid={`toggle-verified-btn-${verifiedPath}`} > @@ -109,4 +92,4 @@ function InputWithStatus({ ); } -export default InputWithStatus; +export default VerifiedInput; diff --git a/src/components/VerifiedQuantityMultiInput.tsx b/src/components/VerifiedQuantityMultiInput.tsx new file mode 100644 index 00000000..43c60ecc --- /dev/null +++ b/src/components/VerifiedQuantityMultiInput.tsx @@ -0,0 +1,301 @@ +import { Quantity } from "@/types/types"; +import AddIcon from "@mui/icons-material/Add"; +import CheckIcon from "@mui/icons-material/Check"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import { + Box, + Button, + Divider, + IconButton, + InputBase, + Tooltip, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import { + Controller, + useFieldArray, + useFormContext, + useWatch, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +function VerifiedQuantityMultiInput({ + label, + placeholder, + path, + unitOptions, + className = "", +}: { + label: string; + placeholder?: string; + path: string; + unitOptions: string[]; + className?: string; +}) { + const { t } = useTranslation("labelDataValidationPage"); + const { control, trigger } = useFormContext(); + const [isFocused, setIsFocused] = useState(false); + + const quantitiesPath = `${path}.quantities`; + const verifiedPath = `${path}.verified`; + + const { fields, append, remove } = useFieldArray({ + control, + name: quantitiesPath, + }); + + const quantities = useWatch({ + control, + name: quantitiesPath, + }); + + const verified: boolean = useWatch({ + control, + name: verifiedPath, + }); + + const toggleVerified = async ( + verified: boolean, + setVerified: (value: boolean) => void, + ) => { + const validationResults = await Promise.all( + fields.map((_, index) => + Promise.all([ + trigger(`${quantitiesPath}.${index}.unit`), + trigger(`${quantitiesPath}.${index}.value`), + ]), + ), + ); + + const allValid = validationResults.every((result) => + result.every((isValid) => isValid), + ); + + if (allValid) { + setVerified(!verified); + } + }; + + const handleAddRow = () => { + const unusedOption = unitOptions.find( + (option) => + !quantities.some((valueItem: Quantity) => valueItem.unit === option), + ); + + if (unusedOption) { + append({ value: "", unit: unusedOption }); + } else { + console.error("All options are already used"); + } + }; + + const validateDuplicateUnit = (value: string) => { + const isDuplicate = + quantities.filter((item: { unit: string }) => item.unit === value) + .length > 1; + return !isDuplicate || t("verifiedQuantityMultiInput.errors.duplicateUnit"); + }; + + return ( + + {/* Label Section */} + + {label} + + + {/* Fields */} + + {fields.map((fieldItem, index) => ( + + + {/* Unit Selection Field */} + ( + <> + + {error && ( + + + + )} + + )} + /> + + {/* Value Input Field */} + ( + <> + setIsFocused(true)} + onBlur={(e) => { + setIsFocused(false); + field.onChange(e.target.value.trim()); + }} + aria-label={t( + "verifiedQuantityMultiInput.accessibility.valueInput", + )} + data-testid={`${quantitiesPath}.${index}.value`} + error={!!error} + /> + {error && ( + + + + )} + + )} + /> + + {/* Delete Row Button */} + + + remove(index)} + disabled={verified} + aria-label={t( + "verifiedQuantityMultiInput.accessibility.deleteRowButton", + )} + data-testid={`delete-button-${quantitiesPath}-${index}`} + > + + + + + + + + ))} + + {/* Add Row Button */} + + + + {/* Vertical Divider */} + + + {/* Verified Toggle Button */} + ( + + toggleVerified(value, onChange)} + aria-label={t( + "verifiedQuantityMultiInput.accessibility.verifyToggleButton", + )} + data-testid={`toggle-verified-btn-${path}`} + > + + + + )} + /> + + ); +} + +export default VerifiedQuantityMultiInput; diff --git a/src/components/__tests__/AlertBanner.test.tsx b/src/components/__tests__/AlertBanner.test.tsx index 71f92836..077edb02 100644 --- a/src/components/__tests__/AlertBanner.test.tsx +++ b/src/components/__tests__/AlertBanner.test.tsx @@ -1,9 +1,33 @@ import useAlertStore from "@/stores/alertStore"; -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { Button } from "@mui/material"; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import AlertBanner from "../AlertBanner"; const AUTO_DISMISS_TIME = - Number(process.env.NEXT_PUBLIC_AUTO_DISMISS_TIME) || 5000; + Number(process.env.NEXT_PUBLIC_ALERT_BANNER_AUTO_DISMISS_TIME) || 5000; + +const AlertWrapper: React.FC = () => { + const { showAlert } = useAlertStore(); + + const handleClick = () => { + showAlert("Test alert message", "success"); + }; + + return ( +
+ + +
+ ); +}; describe("AlertBanner", () => { beforeEach(() => { @@ -79,4 +103,18 @@ describe("AlertBanner", () => { fireEvent.click(screen.getByRole("button")); expect(useAlertStore.getState().alert).toBeNull(); }); + + it("should display the alert banner when showAlert is used", async () => { + render(); + + const button = screen.getByTestId("trigger-alert-button"); + fireEvent.click(button); + + const alert = await waitFor(() => screen.getByTestId("alert-banner")); + + expect(alert).toBeInTheDocument(); + expect(screen.getByTestId("alert-message")).toHaveTextContent( + "Test alert message", + ); + }); }); diff --git a/src/components/__tests__/BaseInformationForm.test.tsx b/src/components/__tests__/BaseInformationForm.test.tsx new file mode 100644 index 00000000..069e438a --- /dev/null +++ b/src/components/__tests__/BaseInformationForm.test.tsx @@ -0,0 +1,53 @@ +import { DEFAULT_LABEL_DATA, LabelData } from "@/types/types"; +import { render, screen } from "@testing-library/react"; +import { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import BaseInformationForm from "../BaseInformationForm"; + +const Wrapper = ({ + initialData, + onStateChange, +}: { + initialData: LabelData; + onStateChange?: (data: LabelData) => void; +}) => { + const [labelData, setLabelData] = useState(initialData); + const methods = useForm({ + defaultValues: labelData, + }); + + useEffect(() => { + if (onStateChange) { + onStateChange(labelData); + } + }, [labelData, onStateChange]); + + return ( + + + + ); +}; + +describe("BaseInformationForm Rendering", () => { + it("should render all fields with correct components", () => { + render(); + + const verifiedFields = ["name", "registrationNumber", "lotNumber", "npk"]; + const quantityFields = ["weight", "density", "volume"]; + + verifiedFields.forEach((key) => { + const verifiedInput = screen.getByTestId( + `verified-input-baseInformation.${key}`, + ); + expect(verifiedInput).toBeInTheDocument(); + }); + + quantityFields.forEach((key) => { + const quantityInput = screen.getByTestId( + `quantity-multi-input-baseInformation.${key}`, + ); + expect(quantityInput).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/__tests__/CautionsForm.test.tsx b/src/components/__tests__/CautionsForm.test.tsx new file mode 100644 index 00000000..00efded1 --- /dev/null +++ b/src/components/__tests__/CautionsForm.test.tsx @@ -0,0 +1,39 @@ +import { DEFAULT_LABEL_DATA, LabelData } from "@/types/types"; +import { render, screen } from "@testing-library/react"; +import { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import CautionsForm from "../CautionsForm"; + +const Wrapper = ({ + initialData, + onStateChange, +}: { + initialData: LabelData; + onStateChange?: (data: LabelData) => void; +}) => { + const [labelData, setLabelData] = useState(initialData); + const methods = useForm({ + defaultValues: labelData, + }); + + useEffect(() => { + if (onStateChange) { + onStateChange(labelData); + } + }, [labelData, onStateChange]); + + return ( + + + + ); +}; + +describe("CautionsForm Rendering", () => { + it("should render the VerifiedBilingualTable for cautions", () => { + render(); + + expect(screen.getByTestId("cautions-form")).toBeInTheDocument(); + expect(screen.getByTestId("table-container-cautions")).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/InputWithStatus.test.tsx b/src/components/__tests__/InputWithStatus.test.tsx deleted file mode 100644 index 421ad210..00000000 --- a/src/components/__tests__/InputWithStatus.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Field, FieldStatus } from "@/types/types"; -import { fireEvent, render, screen } from "@testing-library/react"; -import { FormProvider, useForm } from "react-hook-form"; -import InputWithStatus from "../InputWithStatus"; - -const TestWrapper = ({ defaultStatus }: { defaultStatus: FieldStatus }) => { - const methods = useForm>({ - defaultValues: { - fieldName: { - value: "", - status: defaultStatus, - errorMessage: null, - }, - }, - }); - - return ( - -
- - -
- ); -}; - -describe("Rendering", () => { - it("should render all elements with correct attributes and content", () => { - render(); - - const label = screen.getByTestId("input-label-fieldName.value"); - expect(label).toHaveTextContent("Test Field"); - - const input = screen.getByPlaceholderText("Enter text"); - expect(input).toBeInTheDocument(); - expect(input).toHaveAttribute("name", "fieldName.value"); - expect(input).toHaveValue(""); - - expect( - screen.getByTestId("input-with-status-fieldName.value"), - ).toBeInTheDocument(); - - expect( - screen.getByTestId("toggle-status-btn-fieldName.status"), - ).toBeInTheDocument(); - }); -}); - -describe("Status Behavior", () => { - it("should disable the input field when statusValue is FieldStatus.Verified", () => { - render(); - const input = screen.getByPlaceholderText("Enter text"); - expect(input).toBeDisabled(); - }); - - it("should enable the input field when statusValue is not FieldStatus.Verified", () => { - render(); - const input = screen.getByPlaceholderText("Enter text"); - expect(input).not.toBeDisabled(); - }); - - it("should toggle the status between Verified and Unverified on IconButton click", () => { - render(); - const toggleButton = screen.getByTestId( - "toggle-status-btn-fieldName.status", - ); - const statusIcon = screen.getByTestId("status-icon-fieldName.status"); - expect(statusIcon).not.toHaveClass("text-green-500"); - fireEvent.click(toggleButton); - expect(statusIcon).toHaveClass("text-green-500"); - fireEvent.click(toggleButton); - expect(statusIcon).not.toHaveClass("text-green-500"); - }); - - it("should not toggle the status when the current status is FieldStatus.Error", () => { - render(); - const toggleButton = screen.getByTestId( - "toggle-status-btn-fieldName.status", - ); - const statusIcon = screen.getByTestId("status-icon-fieldName.status"); - expect(statusIcon).not.toHaveClass("text-green-500"); - fireEvent.click(toggleButton); - expect(statusIcon).not.toHaveClass("text-green-500"); - }); -}); diff --git a/src/components/__tests__/InstructionsForm.test.tsx b/src/components/__tests__/InstructionsForm.test.tsx new file mode 100644 index 00000000..c047f95e --- /dev/null +++ b/src/components/__tests__/InstructionsForm.test.tsx @@ -0,0 +1,39 @@ +import { DEFAULT_LABEL_DATA, LabelData } from "@/types/types"; +import { render, screen } from "@testing-library/react"; +import { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import InstructionsForm from "../InstructionsForm"; + +const Wrapper = ({ + initialData, + onStateChange, +}: { + initialData: LabelData; + onStateChange?: (data: LabelData) => void; +}) => { + const [labelData, setLabelData] = useState(initialData); + const methods = useForm({ + defaultValues: labelData, + }); + + useEffect(() => { + if (onStateChange) { + onStateChange(labelData); + } + }, [labelData, onStateChange]); + + return ( + + + + ); +}; + +describe("InstructionsForm Rendering", () => { + it("should render the VerifiedBilingualTable for instructions", () => { + render(); + + expect(screen.getByTestId("instructions-form")).toBeInTheDocument(); + expect(screen.getByTestId("table-container-instructions")).toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/OrganizationsForm.test.tsx b/src/components/__tests__/OrganizationsForm.test.tsx index 95f20a14..2fd3d36c 100644 --- a/src/components/__tests__/OrganizationsForm.test.tsx +++ b/src/components/__tests__/OrganizationsForm.test.tsx @@ -1,6 +1,7 @@ import { + DEFAULT_BASE_INFORMATION, + DEFAULT_LABEL_DATA, DEFAULT_ORGANIZATION, - FieldStatus, LabelData, Organization, } from "@/types/types"; @@ -29,35 +30,19 @@ const Wrapper = ({ return ( - + ); }; describe("OrganizationsForm Rendering", () => { - it("should render the form title", () => { - render( - , - ); - - const title = screen.getByTestId("form-title"); - expect(title).toBeInTheDocument(); - expect(title).toHaveTextContent("Test Organizations"); - }); - it("should render the correct number of organizations", () => { render( , ); @@ -67,13 +52,7 @@ describe("OrganizationsForm Rendering", () => { }); it("should render all inputs for each organization", () => { - render( - , - ); + render(); expect( screen.getByPlaceholderText("Enter organization name"), @@ -89,6 +68,7 @@ describe("OrganizationsForm Rendering", () => { render( , @@ -105,6 +85,7 @@ describe("OrganizationsForm Functionality", () => { render( , @@ -117,13 +98,7 @@ describe("OrganizationsForm Functionality", () => { }); it("should remove an organization when Remove button is clicked", () => { - render( - , - ); + render(); expect(screen.queryAllByTestId(/organization-\d+/)).toHaveLength(1); const removeButton = screen.getByTestId("remove-org-btn-0"); @@ -136,9 +111,7 @@ describe("OrganizationsForm Functionality", () => { render( , ); @@ -164,31 +137,29 @@ describe("OrganizationsForm Functionality", () => { ); }); - it("should update the organization field status when the Verified button is clicked", () => { + it("should update the organization field verification when the Verified button is clicked", () => { const mockStateChange = jest.fn(); render( , ); - const toggleStatusButton = screen.getByTestId( - "toggle-status-btn-organizations.0.address.status", + const verifyButton = screen.getByTestId( + "toggle-verified-btn-organizations.0.address.verified", ); - expect(toggleStatusButton).toBeInTheDocument(); + expect(verifyButton).toBeInTheDocument(); - fireEvent.click(toggleStatusButton); + fireEvent.click(verifyButton); expect(mockStateChange).toHaveBeenCalledWith( expect.objectContaining({ organizations: [ expect.objectContaining({ address: expect.objectContaining({ - status: FieldStatus.Verified, + verified: true, }), }), ], @@ -201,9 +172,7 @@ describe("OrganizationsForm Functionality", () => { render( , ); @@ -218,16 +187,16 @@ describe("OrganizationsForm Functionality", () => { organizations: [ expect.objectContaining({ name: expect.objectContaining({ - status: FieldStatus.Verified, + verified: true, }), address: expect.objectContaining({ - status: FieldStatus.Verified, + verified: true, }), website: expect.objectContaining({ - status: FieldStatus.Verified, + verified: true, }), phoneNumber: expect.objectContaining({ - status: FieldStatus.Verified, + verified: true, }), }), ], @@ -239,29 +208,26 @@ describe("OrganizationsForm Functionality", () => { const verifiedOrg: Organization = { name: { value: "", - status: FieldStatus.Verified, - errorMessage: null, + verified: true, }, address: { value: "", - status: FieldStatus.Verified, - errorMessage: null, + verified: true, }, website: { value: "", - status: FieldStatus.Verified, - errorMessage: null, + verified: true, }, phoneNumber: { value: "", - status: FieldStatus.Verified, - errorMessage: null, + verified: true, }, }; render( , @@ -277,29 +243,26 @@ describe("OrganizationsForm Functionality", () => { const partiallyVerifiedOrg = { name: { value: "Test Name", - status: FieldStatus.Verified, - errorMessage: null, + verified: true, }, address: { value: "123 Test St", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, website: { value: "https://test.com", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, phoneNumber: { value: "123-456-7890", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, }; render( { organizations: [ expect.objectContaining({ name: expect.objectContaining({ - status: FieldStatus.Unverified, + verified: false, }), address: expect.objectContaining({ - status: FieldStatus.Unverified, + verified: false, }), website: expect.objectContaining({ - status: FieldStatus.Unverified, + verified: false, }), phoneNumber: expect.objectContaining({ - status: FieldStatus.Unverified, + verified: false, }), }), ], @@ -338,29 +301,26 @@ describe("OrganizationsForm Functionality", () => { const allUnverifiedOrg = { name: { value: "Test Name", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, address: { value: "123 Test St", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, website: { value: "https://test.com", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, phoneNumber: { value: "123-456-7890", - status: FieldStatus.Unverified, - errorMessage: null, + verified: false, }, }; render( , @@ -376,6 +336,7 @@ describe("OrganizationsForm Edge Cases", () => { render( , diff --git a/src/components/__tests__/Sidenav.test.tsx b/src/components/__tests__/Sidenav.test.tsx index 4344e86b..ca34b392 100644 --- a/src/components/__tests__/Sidenav.test.tsx +++ b/src/components/__tests__/Sidenav.test.tsx @@ -89,7 +89,6 @@ describe("SideNav Component", () => { screen.getByText(t("sideNav.repportIssue")).closest("a"), ).toHaveAttribute( "href", - "https://github.com/ai-cfia/fertiscan-frontend/issues/new/choose", - ); + ); }); }); diff --git a/src/components/__tests__/VerifiedBilingualTable.test.tsx b/src/components/__tests__/VerifiedBilingualTable.test.tsx new file mode 100644 index 00000000..a7b35d97 --- /dev/null +++ b/src/components/__tests__/VerifiedBilingualTable.test.tsx @@ -0,0 +1,240 @@ +import { BilingualField } from "@/types/types"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { FormProvider, useForm } from "react-hook-form"; +import VerifiedBilingualTable from "../VerifiedBilingualTable"; + +const Wrapper = ({ + path = "bilingualFields", + defaultValues = { + bilingualFields: [ + { en: "English Text", fr: "French Text", verified: false }, + ], + }, + onSubmit = jest.fn(), +}: { + path?: string; + defaultValues?: { + bilingualFields: { en: string; fr: string; verified: boolean }[]; + }; + onSubmit?: (data: { bilingualFields: BilingualField[] }) => void; +}) => { + const methods = useForm({ + defaultValues, + mode: "onSubmit", + }); + + return ( + +
{ + event.preventDefault(); + methods.handleSubmit(onSubmit)(); + }} + > + + + +
+ ); +}; + +describe("VerifiedBilingualTable rendering and functionality", () => { + it("renders with default values", () => { + render(); + + expect( + screen.getByTestId("table-header-english-bilingualFields"), + ).toHaveTextContent("verifiedBilingualTable.english"); + expect( + screen.getByTestId("table-header-french-bilingualFields"), + ).toHaveTextContent("verifiedBilingualTable.french"); + expect( + screen.getByTestId("table-header-actions-bilingualFields"), + ).toHaveTextContent("verifiedBilingualTable.actions"); + + const rows = screen.getAllByTestId(/table-row-bilingualFields-\d+/); + expect(rows.length).toBe(1); + + const englishInputBase = screen.getByTestId( + "input-english-bilingualFields-0", + ); + const englishInput = englishInputBase.querySelector( + "textarea", + ) as HTMLTextAreaElement; + const frenchInputBase = screen.getByTestId( + "input-french-bilingualFields-0", + ); + const frenchInput = frenchInputBase.querySelector( + "textarea", + ) as HTMLTextAreaElement; + + expect(englishInput.value).toBe("English Text"); + expect(frenchInput.value).toBe("French Text"); + }); + + it("adds a new row when Add button is clicked", () => { + render(); + + fireEvent.click(screen.getByTestId("add-row-btn-bilingualFields")); + + const rows = screen.getAllByTestId(/table-row-bilingualFields-\d+/); + expect(rows.length).toBe(2); + + const englishInputBase = screen.getByTestId( + "input-english-bilingualFields-1", + ); + const englishInput = englishInputBase.querySelector( + "textarea", + ) as HTMLTextAreaElement; + const frenchInputBase = screen.getByTestId( + "input-french-bilingualFields-1", + ); + const frenchInput = frenchInputBase.querySelector( + "textarea", + ) as HTMLTextAreaElement; + + expect(englishInput.value).toBe(""); + expect(frenchInput.value).toBe(""); + }); + + it("deletes a row when Delete button is clicked", () => { + render(); + + fireEvent.click(screen.getByTestId("delete-row-btn-bilingualFields-0")); + + const rows = screen.queryAllByTestId(/table-row-bilingualFields-\d+/); + expect(rows.length).toBe(0); + }); + + it("marks all rows as verified when Verify All button is clicked", () => { + render(); + + fireEvent.click(screen.getByTestId("verify-all-btn-bilingualFields")); + + const verifyButtons = screen.getAllByTestId( + /verify-row-btn-bilingualFields-\d+/, + ); + verifyButtons.forEach((button) => { + const icon = button.querySelector("svg"); + expect(icon).toHaveClass("text-green-500"); + }); + + const inputs = screen.getAllByTestId( + /input-(english|french)-bilingualFields-\d+/, + ); + inputs.forEach((baseInput) => { + const textarea = baseInput.querySelector( + "textarea", + ) as HTMLTextAreaElement; + expect(textarea).toBeDisabled(); + }); + + const deleteButtons = screen.getAllByTestId( + /delete-row-btn-bilingualFields-\d+/, + ); + deleteButtons.forEach((button) => { + expect(button).toBeDisabled(); + }); + }); + + it("marks all rows as unverified when Unverify All button is clicked", () => { + const defaultValues = { + bilingualFields: [ + { en: "Verified English 1", fr: "Verified French 1", verified: true }, + { en: "Verified English 2", fr: "Verified French 2", verified: true }, + ], + }; + + render(); + + const verifyButtonsBefore = screen.getAllByTestId( + /verify-row-btn-bilingualFields-\d+/, + ); + verifyButtonsBefore.forEach((button) => { + const icon = button.querySelector("svg"); + expect(icon).toHaveClass("text-green-500"); + }); + + const inputsBefore = screen.getAllByTestId( + /input-(english|french)-bilingualFields-\d+/, + ); + inputsBefore.forEach((baseInput) => { + const textarea = baseInput.querySelector( + "textarea", + ) as HTMLTextAreaElement; + expect(textarea).toBeDisabled(); + }); + + const deleteButtonsBefore = screen.getAllByTestId( + /delete-row-btn-bilingualFields-\d+/, + ); + deleteButtonsBefore.forEach((button) => { + expect(button).toBeDisabled(); + }); + + fireEvent.click(screen.getByTestId("unverify-all-btn-bilingualFields")); + + const verifyButtonsAfter = screen.getAllByTestId( + /verify-row-btn-bilingualFields-\d+/, + ); + verifyButtonsAfter.forEach((button) => { + const icon = button.querySelector("svg"); + expect(icon).not.toHaveClass("text-green-500"); + }); + + const inputsAfter = screen.getAllByTestId( + /input-(english|french)-bilingualFields-\d+/, + ); + inputsAfter.forEach((baseInput) => { + const textarea = baseInput.querySelector( + "textarea", + ) as HTMLTextAreaElement; + expect(textarea).not.toBeDisabled(); + }); + + const deleteButtonsAfter = screen.getAllByTestId( + /delete-row-btn-bilingualFields-\d+/, + ); + deleteButtonsAfter.forEach((button) => { + expect(button).not.toBeDisabled(); + }); + }); + + it("submits correct data on form submit", async () => { + const onSubmit = jest.fn(); + render(); + + const englishTextarea = screen + .getByTestId("input-english-bilingualFields-0") + .querySelector("textarea") as HTMLTextAreaElement; + + const frenchTextarea = screen + .getByTestId("input-french-bilingualFields-0") + .querySelector("textarea") as HTMLTextAreaElement; + + fireEvent.change(englishTextarea, { + target: { value: "Updated English Text" }, + }); + fireEvent.change(frenchTextarea, { + target: { value: "Updated French Text" }, + }); + + fireEvent.click(screen.getByTestId("submit-button")); + + await waitFor(() => { + expect(onSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + bilingualFields: [ + { + en: "Updated English Text", + fr: "Updated French Text", + verified: false, + }, + ], + }), + ); + }); + }); +}); diff --git a/src/components/__tests__/VerifiedInput.test.tsx b/src/components/__tests__/VerifiedInput.test.tsx new file mode 100644 index 00000000..39ddd3cb --- /dev/null +++ b/src/components/__tests__/VerifiedInput.test.tsx @@ -0,0 +1,77 @@ +import { VerifiedTextField } from "@/types/types"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { FormProvider, useForm } from "react-hook-form"; +import VerifiedInput from "../VerifiedInput"; + +const TestWrapper = ({ verified }: { verified: boolean }) => { + const methods = useForm>({ + defaultValues: { + fieldName: { + value: "", + verified: verified, + }, + }, + }); + + return ( + +
+ + +
+ ); +}; + +describe("Rendering", () => { + it("should render all elements with correct attributes and content", () => { + render(); + + const label = screen.getByTestId("input-label-fieldName"); + expect(label).toHaveTextContent("Test Field"); + + const input = screen.getByPlaceholderText("Enter text"); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("name", "fieldName.value"); + expect(input).toHaveValue(""); + + expect( + screen.getByTestId("input-field-fieldName.value"), + ).toBeInTheDocument(); + + expect( + screen.getByTestId("toggle-verified-btn-fieldName.verified"), + ).toBeInTheDocument(); + }); +}); + +describe("Verified Behavior", () => { + it("should disable the input field when verifiedValue is true", () => { + render(); + const input = screen.getByPlaceholderText("Enter text"); + expect(input).toBeDisabled(); + }); + + it("should enable the input field when verifiedValue is false", () => { + render(); + const input = screen.getByPlaceholderText("Enter text"); + expect(input).not.toBeDisabled(); + }); + + it("should toggle verified between true and false on IconButton click", () => { + render(); + const toggleButton = screen.getByTestId( + "toggle-verified-btn-fieldName.verified", + ); + const verifiedIcon = screen.getByTestId("verified-icon-fieldName.verified"); + expect(verifiedIcon).not.toHaveClass("text-green-500"); + fireEvent.click(toggleButton); + expect(verifiedIcon).toHaveClass("text-green-500"); + fireEvent.click(toggleButton); + expect(verifiedIcon).not.toHaveClass("text-green-500"); + }); +}); diff --git a/src/components/__tests__/VerifiedQuantityMultiInput.test.tsx b/src/components/__tests__/VerifiedQuantityMultiInput.test.tsx new file mode 100644 index 00000000..bbe53d2c --- /dev/null +++ b/src/components/__tests__/VerifiedQuantityMultiInput.test.tsx @@ -0,0 +1,355 @@ +import { VerifiedQuantityField } from "@/types/types"; +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { FormProvider, useForm } from "react-hook-form"; +import VerifiedQuantityMultiInput from "../VerifiedQuantityMultiInput"; + +const Wrapper = ({ + label = "Test Label", + placeholder = "Enter a value", + path = "", + unitOptions = ["kg", "lb", "oz", "ton"], + defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }, + onSubmit = jest.fn(), +}: { + label?: string; + placeholder?: string; + path?: string; + unitOptions?: string[]; + defaultValues?: VerifiedQuantityField; + onSubmit?: (data: VerifiedQuantityField) => void; +}) => { + const methods = useForm({ + defaultValues, + mode: "onSubmit", + }); + + return ( + +
{ + event.preventDefault(); + methods.handleSubmit(onSubmit)(); + }} + > + + + +
+ ); +}; + +describe("QuantityMultiInput rendering", () => { + it("renders correctly with default settings", () => { + render(); + + expect(screen.getByTestId("quantity-multi-input-label-")).toHaveTextContent( + "Test Label", + ); + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + expect(screen.getByTestId("toggle-verified-btn-")).toBeInTheDocument(); + + const dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(1); + expect(dropdowns[0].children.length).toBeGreaterThan(0); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(1); + + const inputField = inputFields[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("0"); + + const removeButtons = screen.getAllByTestId( + /delete-button-\.quantities-\d+/, + ); + expect(removeButtons.length).toBe(1); + + expect(screen.getByTestId("add-button-")).toBeInTheDocument(); + }); + + it("renders initial rows and dropdown options correctly based on defaultValues", () => { + const defaultValues = { + quantities: [ + { value: "10", unit: "kg" }, + { value: "20", unit: "lb" }, + ], + verified: false, + }; + render(); + + const fieldRows = screen.getAllByTestId(/field-row-\.quantities-\d+/); + expect(fieldRows.length).toBe(2); + + const inputs = fieldRows.map( + (row) => + row.querySelector( + "[data-testid^='.quantities.'][data-testid$='.value'] input", + ) as HTMLInputElement, + ); + expect(inputs[0].value).toBe("10"); + expect(inputs[1].value).toBe("20"); + + const dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(2); + expect(dropdowns[0].children.length).toBeGreaterThan(0); + }); +}); + +describe("QuantityMultiInput functionality", () => { + it("disables inputs rows and add row button when status is Verified", () => { + const defaultValues = { + quantities: [{ value: "50", unit: "kg" }], + verified: true, + }; + render(); + + screen.getAllByTestId(/quantities\.\d+\.value/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + screen.getAllByTestId(/quantities\.\d+\.unit/).forEach((dropdown) => { + expect(dropdown).toBeDisabled(); + }); + screen + .getAllByTestId(/delete-button-\.quantities-\d+/) + .forEach((button) => { + expect(button).toBeDisabled(); + }); + expect(screen.getByTestId("add-button-")).toBeDisabled(); + + userEvent.click(screen.getByTestId("toggle-verified-btn-")); + + screen.getAllByTestId(/quantities\.\d+\.value/).forEach((field) => { + expect(field.querySelector("input")).toBeDisabled(); + }); + screen.getAllByTestId(/quantities\.\d+\.unit/).forEach((dropdown) => { + expect(dropdown).toBeDisabled(); + }); + screen + .getAllByTestId(/delete-button-\.quantities-\d+/) + .forEach((button) => { + expect(button).toBeDisabled(); + }); + expect(screen.getByTestId("add-button-")).toBeDisabled(); + }); + + it("handles Add and Remove row functionality", () => { + const defaultValues = { + quantities: [ + { value: "10", unit: "kg" }, + { value: "20", unit: "lb" }, + ], + verified: false, + }; + render(); + + let fieldRows = screen.getAllByTestId(/field-row-\.quantities-\d+/); + expect(fieldRows.length).toBe(2); + + const removeButtons = screen.getAllByTestId( + /delete-button-\.quantities-\d+/, + ); + fireEvent.click(removeButtons[0]); + + fieldRows = screen.queryAllByTestId(/field-row-\.quantities-\d+/); + expect(fieldRows.length).toBe(1); + + const addButton = screen.getByTestId("add-button-"); + fireEvent.click(addButton); + + fieldRows = screen.getAllByTestId(/field-row-\.quantities-\d+/); + expect(fieldRows.length).toBe(2); + }); + + it("shows an error for negative values", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }; + + render(); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(1); + + const inputField = inputFields[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("0"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "-10"); + + await userEvent.click(screen.getByTestId("verified-icon-")); + + expect( + screen.getByTestId(/value-error-icon-\.quantities-\d+/), + ).toBeInTheDocument(); + + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); + + it("shows an error for non-numeric values", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }; + + render(); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(1); + + const inputField = inputFields[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("0"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "abc"); + + await userEvent.click(screen.getByTestId("verified-icon-")); + + expect( + screen.getByTestId(/value-error-icon-\.quantities-\d+/), + ).toBeInTheDocument(); + + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); + + it("shows an error for duplicate units", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + quantities: [ + { value: "10", unit: "kg" }, + { value: "20", unit: "kg" }, + ], + verified: false, + }; + + render(); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(2); + + const inputField = inputFields[1].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("20"); + + await userEvent.click(screen.getByTestId("submit-button")); + + expect( + screen.getAllByTestId(/unit-error-icon-\.quantities-\d+/).length, + ).toBe(2); + + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); + + it("calls onSubmit with correct values", async () => { + const mockOnSubmit = jest.fn(); + const defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }; + + render(); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(1); + + const inputField = inputFields[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("0"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "10"); + + const dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(1); + const dropdown = dropdowns[0] as HTMLSelectElement; + await userEvent.selectOptions(dropdown, "lb"); + + await userEvent.click(screen.getByTestId("toggle-verified-btn-")); + + await userEvent.click(screen.getByTestId("submit-button")); + + expect(mockOnSubmit.mock.calls[0][0]).toEqual( + expect.objectContaining({ + quantities: [{ value: "10", unit: "lb" }], + verified: true, + }), + ); + }); + + it("doesn't allow toggling status when there are errors", async () => { + const defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }; + + render(); + + const inputFields = screen.getAllByTestId(/quantities\.\d+\.value/); + expect(inputFields.length).toBe(1); + + const inputField = inputFields[0].querySelector( + "input", + ) as HTMLInputElement; + expect(inputField).toHaveValue("0"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "abc"); + + const toggleButton = screen.getByTestId("verified-icon-"); + await userEvent.click(toggleButton); + expect(toggleButton).not.toHaveClass("text-green-500"); + + await userEvent.clear(inputField); + await userEvent.type(inputField, "10"); + await userEvent.click(toggleButton); + expect(toggleButton).toHaveClass("text-green-500"); + }); + + it("adds a row with a different unit every time add button is clicked", async () => { + const defaultValues = { + quantities: [{ value: "0", unit: "kg" }], + verified: false, + }; + + render(); + + const addButton = screen.getByTestId("add-button-"); + + await userEvent.click(addButton); + let dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(2); + expect(dropdowns[1]).toHaveValue("lb"); + + await userEvent.click(addButton); + dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(3); + expect(dropdowns[2]).toHaveValue("oz"); + + await userEvent.click(addButton); + dropdowns = screen.getAllByTestId(/quantities\.\d+\.unit/); + expect(dropdowns.length).toBe(4); + expect(dropdowns[3]).toHaveValue("ton"); + + expect(addButton).toBeDisabled(); + }); +}); diff --git a/src/stores/alertStore.ts b/src/stores/alertStore.ts index b15d1b2b..14d0b426 100644 --- a/src/stores/alertStore.ts +++ b/src/stores/alertStore.ts @@ -1,4 +1,4 @@ -import Alert, { AlertSeverity } from "@/types/Alert"; +import { Alert, AlertSeverity } from "@/types/types"; import { create } from "zustand"; interface AlertState { diff --git a/src/types/types.ts b/src/types/types.ts index c5bca22a..70b636b5 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -59,99 +59,110 @@ export interface Alert { type: AlertSeverity; } -// Field -export enum FieldStatus { - Verified = "verified", - Unverified = "unverified", - Error = "error", -} +export type VerifiedField = { + verified: boolean; +}; -export type Field = { +export type VerifiedTextField = VerifiedField & { value: string; - status: FieldStatus; - errorMessage: string | null; }; // Organizations export type Organization = { - name: Field; - address: Field; - website: Field; - phoneNumber: Field; + name: VerifiedTextField; + address: VerifiedTextField; + website: VerifiedTextField; + phoneNumber: VerifiedTextField; +}; + +const DEFAULT_TEXT_FIELD: VerifiedTextField = { + value: "", + verified: false, }; export const DEFAULT_ORGANIZATION: Organization = { - name: { - value: "", - status: FieldStatus.Unverified, - errorMessage: null, - }, - address: { - value: "", - status: FieldStatus.Unverified, - errorMessage: null, - }, - website: { - value: "", - status: FieldStatus.Unverified, - errorMessage: null, - }, - phoneNumber: { - value: "", - status: FieldStatus.Unverified, - errorMessage: null, - }, + name: DEFAULT_TEXT_FIELD, + address: DEFAULT_TEXT_FIELD, + website: DEFAULT_TEXT_FIELD, + phoneNumber: DEFAULT_TEXT_FIELD, +}; + +export const isVerified = >( + fields: T, + verified: boolean = true, +): boolean => + fields && Object.values(fields).every((field) => field.verified === verified); + +// Quantity +export type Quantity = { + value: string; + unit: string; +}; + +export type VerifiedQuantityField = VerifiedField & { + quantities: Quantity[]; +}; + +export const UNITS = { + weight: ["kg", "g", "lb", "tonne"], + volume: ["L", "mL", "gal", "ft³"], + density: ["lb/ft³", "g/cm³", "kg/m³", "lb/gal"], +}; + +const DEFAULT_QUANTITY_FIELD = (unit: string): VerifiedQuantityField => ({ + quantities: [{ value: "", unit }], + verified: false, +}); + +// Base Information +export type BaseInformation = { + name: VerifiedTextField; + registrationNumber: VerifiedTextField; + lotNumber: VerifiedTextField; + npk: VerifiedTextField; + weight: VerifiedQuantityField; + density: VerifiedQuantityField; + volume: VerifiedQuantityField; }; -export const TEST_ORGANIZATION: Organization = { - name: { - value: "GreenGrow Inc.", - status: FieldStatus.Unverified, - errorMessage: null, - }, - address: { - value: "123 Green Road, Farmville, State, 12345", - status: FieldStatus.Unverified, - errorMessage: null, - }, - website: { - value: "https://www.greengrow.com", - status: FieldStatus.Unverified, - errorMessage: null, - }, - phoneNumber: { - value: "123-456-7890", - status: FieldStatus.Unverified, - errorMessage: null, - }, +export const DEFAULT_BASE_INFORMATION: BaseInformation = { + name: DEFAULT_TEXT_FIELD, + registrationNumber: DEFAULT_TEXT_FIELD, + lotNumber: DEFAULT_TEXT_FIELD, + npk: DEFAULT_TEXT_FIELD, + weight: DEFAULT_QUANTITY_FIELD(UNITS.weight[0]), + density: DEFAULT_QUANTITY_FIELD(UNITS.density[0]), + volume: DEFAULT_QUANTITY_FIELD(UNITS.volume[0]), }; -export const checkOrganizationStatus = ( - organization: Organization, - status: FieldStatus, -): boolean => { - return ( - organization && - Object.values(organization).every((field) => field.status === status) - ); +export type BilingualField = VerifiedField & { + en: string; + fr: string; +}; + +export const DEFAULT_BILINGUAL_FIELD: BilingualField = { + en: "", + fr: "", + verified: false, }; // LabelData export type LabelData = { organizations: Organization[]; + baseInformation: BaseInformation; + cautions: BilingualField[]; + instructions: BilingualField[]; }; export const DEFAULT_LABEL_DATA: LabelData = { organizations: [DEFAULT_ORGANIZATION], + baseInformation: DEFAULT_BASE_INFORMATION, + cautions: [DEFAULT_BILINGUAL_FIELD], + instructions: [DEFAULT_BILINGUAL_FIELD], }; -export const TEST_LABEL_DATA: LabelData = { - organizations: [TEST_ORGANIZATION, TEST_ORGANIZATION], -}; - -// Form +// Form export interface FormComponentProps { - title: string; labelData: LabelData; setLabelData: React.Dispatch>; } diff --git a/test.txt b/test.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +