This commit is contained in:
Philippe Torrel
2026-08-24 11:56:41 +02:00
parent a5e74fd729
commit 821c839574
71 changed files with 19772 additions and 248 deletions

View File

@@ -18,16 +18,16 @@
"devDependencies": {
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.4",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"@vitest/ui": "^4.1.10",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"jsdom": "^30.0.1",
"oxlint": "^1.78.0",
"sass-embedded": "^1.102.0",
"vite": "^8.2.1",
"vitest": "^4.1.10"
"oxlint": "^1.79.0",
"sass-embedded": "^1.103.1",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
},
"node_modules/@adobe/css-tools": {
@@ -303,9 +303,9 @@
}
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz",
"integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz",
"integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==",
"cpu": [
"arm"
],
@@ -320,9 +320,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz",
"integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz",
"integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==",
"cpu": [
"arm64"
],
@@ -337,9 +337,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz",
"integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz",
"integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==",
"cpu": [
"arm64"
],
@@ -354,9 +354,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz",
"integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz",
"integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==",
"cpu": [
"x64"
],
@@ -371,9 +371,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz",
"integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz",
"integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==",
"cpu": [
"x64"
],
@@ -388,9 +388,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz",
"integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz",
"integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==",
"cpu": [
"arm"
],
@@ -405,9 +405,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz",
"integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz",
"integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==",
"cpu": [
"arm"
],
@@ -422,9 +422,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz",
"integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz",
"integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==",
"cpu": [
"arm64"
],
@@ -442,9 +442,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz",
"integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz",
"integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==",
"cpu": [
"arm64"
],
@@ -462,9 +462,9 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz",
"integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz",
"integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==",
"cpu": [
"ppc64"
],
@@ -482,9 +482,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz",
"integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz",
"integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==",
"cpu": [
"riscv64"
],
@@ -502,9 +502,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz",
"integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz",
"integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==",
"cpu": [
"riscv64"
],
@@ -522,9 +522,9 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz",
"integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz",
"integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==",
"cpu": [
"s390x"
],
@@ -542,9 +542,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz",
"integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz",
"integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==",
"cpu": [
"x64"
],
@@ -562,9 +562,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz",
"integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz",
"integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==",
"cpu": [
"x64"
],
@@ -582,9 +582,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz",
"integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz",
"integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==",
"cpu": [
"arm64"
],
@@ -599,9 +599,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz",
"integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz",
"integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==",
"cpu": [
"arm64"
],
@@ -616,9 +616,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz",
"integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz",
"integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==",
"cpu": [
"ia32"
],
@@ -633,9 +633,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz",
"integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz",
"integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==",
"cpu": [
"x64"
],
@@ -1329,9 +1329,9 @@
}
},
"node_modules/@testing-library/user-event": {
"version": "14.6.4",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz",
"integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==",
"version": "14.6.6",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz",
"integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1386,9 +1386,9 @@
}
},
"node_modules/@types/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1396,9 +1396,9 @@
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
"integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz",
"integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1410,6 +1410,7 @@
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"oxc-transform-react": "^0.145.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
@@ -1418,20 +1419,23 @@
},
"babel-plugin-react-compiler": {
"optional": true
},
"oxc-transform-react": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
"integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/spy": "4.1.11",
"@vitest/utils": "4.1.11",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1440,13 +1444,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
"integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.10",
"@vitest/spy": "4.1.11",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1467,9 +1471,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
"integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1480,13 +1484,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
"integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.10",
"@vitest/utils": "4.1.11",
"pathe": "^2.0.3"
},
"funding": {
@@ -1494,14 +1498,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
"integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/pretty-format": "4.1.11",
"@vitest/utils": "4.1.11",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1510,9 +1514,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
"integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1520,13 +1524,13 @@
}
},
"node_modules/@vitest/ui": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.10.tgz",
"integrity": "sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.11.tgz",
"integrity": "sha512-r/rwyKoev21mWdRGSEkZOqkQ2BYy68mwjihg9M90nNRbf4NGrgzZ4cj6JNCEwlOGJkbKeMgsjlykvwKUbRr7gw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.10",
"@vitest/utils": "4.1.11",
"fflate": "^0.8.2",
"flatted": "^3.4.2",
"pathe": "^2.0.3",
@@ -1538,17 +1542,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.1.10"
"vitest": "4.1.11"
}
},
"node_modules/@vitest/utils": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
"integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.10",
"@vitest/pretty-format": "4.1.11",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -2670,9 +2674,9 @@
}
},
"node_modules/oxlint": {
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz",
"integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==",
"version": "1.79.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz",
"integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2685,25 +2689,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxlint/binding-android-arm-eabi": "1.78.0",
"@oxlint/binding-android-arm64": "1.78.0",
"@oxlint/binding-darwin-arm64": "1.78.0",
"@oxlint/binding-darwin-x64": "1.78.0",
"@oxlint/binding-freebsd-x64": "1.78.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.78.0",
"@oxlint/binding-linux-arm-musleabihf": "1.78.0",
"@oxlint/binding-linux-arm64-gnu": "1.78.0",
"@oxlint/binding-linux-arm64-musl": "1.78.0",
"@oxlint/binding-linux-ppc64-gnu": "1.78.0",
"@oxlint/binding-linux-riscv64-gnu": "1.78.0",
"@oxlint/binding-linux-riscv64-musl": "1.78.0",
"@oxlint/binding-linux-s390x-gnu": "1.78.0",
"@oxlint/binding-linux-x64-gnu": "1.78.0",
"@oxlint/binding-linux-x64-musl": "1.78.0",
"@oxlint/binding-openharmony-arm64": "1.78.0",
"@oxlint/binding-win32-arm64-msvc": "1.78.0",
"@oxlint/binding-win32-ia32-msvc": "1.78.0",
"@oxlint/binding-win32-x64-msvc": "1.78.0"
"@oxlint/binding-android-arm-eabi": "1.79.0",
"@oxlint/binding-android-arm64": "1.79.0",
"@oxlint/binding-darwin-arm64": "1.79.0",
"@oxlint/binding-darwin-x64": "1.79.0",
"@oxlint/binding-freebsd-x64": "1.79.0",
"@oxlint/binding-linux-arm-gnueabihf": "1.79.0",
"@oxlint/binding-linux-arm-musleabihf": "1.79.0",
"@oxlint/binding-linux-arm64-gnu": "1.79.0",
"@oxlint/binding-linux-arm64-musl": "1.79.0",
"@oxlint/binding-linux-ppc64-gnu": "1.79.0",
"@oxlint/binding-linux-riscv64-gnu": "1.79.0",
"@oxlint/binding-linux-riscv64-musl": "1.79.0",
"@oxlint/binding-linux-s390x-gnu": "1.79.0",
"@oxlint/binding-linux-x64-gnu": "1.79.0",
"@oxlint/binding-linux-x64-musl": "1.79.0",
"@oxlint/binding-openharmony-arm64": "1.79.0",
"@oxlint/binding-win32-arm64-msvc": "1.79.0",
"@oxlint/binding-win32-ia32-msvc": "1.79.0",
"@oxlint/binding-win32-x64-msvc": "1.79.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=7.0.2001",
@@ -2943,9 +2947,9 @@
}
},
"node_modules/sass": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz",
"integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.103.1.tgz",
"integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -2965,9 +2969,9 @@
}
},
"node_modules/sass-embedded": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.102.0.tgz",
"integrity": "sha512-7xZ7jbsGTpA+oRIJ3HKeiYy+RIrwHltIjKMdnRTAZiPpqvKzwy80zZqR532yk5bpHn1lTqWdZ6Fh4KI+GWSsKA==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.103.1.tgz",
"integrity": "sha512-UVIuFZPzz+4DXYbHzQ5gKaKkyP2IH3Sc/WoF63N2V7QIfnC2E9UpQAtiKlCVJTfBe43HRgHDBLnXpZkYAEBsNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2986,30 +2990,30 @@
"node": ">=20.19.0"
},
"optionalDependencies": {
"sass-embedded-all-unknown": "1.102.0",
"sass-embedded-android-arm": "1.102.0",
"sass-embedded-android-arm64": "1.102.0",
"sass-embedded-android-riscv64": "1.102.0",
"sass-embedded-android-x64": "1.102.0",
"sass-embedded-darwin-arm64": "1.102.0",
"sass-embedded-darwin-x64": "1.102.0",
"sass-embedded-linux-arm": "1.102.0",
"sass-embedded-linux-arm64": "1.102.0",
"sass-embedded-linux-musl-arm": "1.102.0",
"sass-embedded-linux-musl-arm64": "1.102.0",
"sass-embedded-linux-musl-riscv64": "1.102.0",
"sass-embedded-linux-musl-x64": "1.102.0",
"sass-embedded-linux-riscv64": "1.102.0",
"sass-embedded-linux-x64": "1.102.0",
"sass-embedded-unknown-all": "1.102.0",
"sass-embedded-win32-arm64": "1.102.0",
"sass-embedded-win32-x64": "1.102.0"
"sass-embedded-all-unknown": "1.103.1",
"sass-embedded-android-arm": "1.103.1",
"sass-embedded-android-arm64": "1.103.1",
"sass-embedded-android-riscv64": "1.103.1",
"sass-embedded-android-x64": "1.103.1",
"sass-embedded-darwin-arm64": "1.103.1",
"sass-embedded-darwin-x64": "1.103.1",
"sass-embedded-linux-arm": "1.103.1",
"sass-embedded-linux-arm64": "1.103.1",
"sass-embedded-linux-musl-arm": "1.103.1",
"sass-embedded-linux-musl-arm64": "1.103.1",
"sass-embedded-linux-musl-riscv64": "1.103.1",
"sass-embedded-linux-musl-x64": "1.103.1",
"sass-embedded-linux-riscv64": "1.103.1",
"sass-embedded-linux-x64": "1.103.1",
"sass-embedded-unknown-all": "1.103.1",
"sass-embedded-win32-arm64": "1.103.1",
"sass-embedded-win32-x64": "1.103.1"
}
},
"node_modules/sass-embedded-all-unknown": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.102.0.tgz",
"integrity": "sha512-BpaRCODYILywdCOI8++Q63+/ptsnWdwYuKyD5gMH0cJblXj85EePdeTTOM7Hl2PP6d4dIojg8GS9fIZhD2x8qA==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.103.1.tgz",
"integrity": "sha512-MwX2ap06TbImAqlbnH6EndgBBejk6Eq2QB6Ae2hukCn9wmXf9cRu27um3mz3tyB4oJOOku0xfHqFH/+ckYj6YA==",
"cpu": [
"!arm",
"!arm64",
@@ -3020,13 +3024,13 @@
"license": "MIT",
"optional": true,
"dependencies": {
"sass": "1.102.0"
"sass": "1.103.1"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.102.0.tgz",
"integrity": "sha512-QM6sPMUtUvFopVR15qXWdobSTYW/sPrXJLxa7eaIbqr/0EtNBZirEXdiOFdDNcsFIEFX3NeZjDq8ZkBAQrxR1w==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.103.1.tgz",
"integrity": "sha512-p36WHpsu5HEo2+NVNbI2Nmb8VgVH0xwOQyzghvZhF7ikGCzEVYI5BP28vHUZlCcgj2TUvMEJKI2lV336a+F/sg==",
"cpu": [
"arm"
],
@@ -3041,9 +3045,9 @@
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.102.0.tgz",
"integrity": "sha512-p34iJl04ksTA/6/NZNKv4Bda710hvpoUNUK6vgHKe53tTGhL5XOfYyTjX95lpKPSvGm4BlPEVSSo6CBhyNSDpw==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.103.1.tgz",
"integrity": "sha512-jBXWMksyz55XeLOWvQs64BqiG5DcqflmMxcvMoA2oHtQT/7XZ02hBn502qpcF/q8Qd+qqG/hU5ccM+hGXrbQZw==",
"cpu": [
"arm64"
],
@@ -3058,9 +3062,9 @@
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.102.0.tgz",
"integrity": "sha512-wcT5N3B6fIwCKCicDcUP6Jol+e1R3WxHMGU7Swp9Ds9VV8di7HYOKIFcasT25dKJv4l17SA7x5Oum0FlRnMPkg==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.103.1.tgz",
"integrity": "sha512-rzrH7RNntk0rx+zCE9xOiAIUt37D1cQKvFfcoX0xiePdGbNHBN8DWKaWN9vaCRfbgKCmxPjevzaqFNI/G/8E1A==",
"cpu": [
"riscv64"
],
@@ -3075,9 +3079,9 @@
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.102.0.tgz",
"integrity": "sha512-IF4eJmoM4uzOborahHp8D4PlCIvumr6IGEbe1XkedwE57qRdk/GENiYZjGwYUQP0uy+pfb+WfvTNhMxkD2VhJg==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.103.1.tgz",
"integrity": "sha512-EQglAJXJutuIcqgZf7YOwzao2ND6ow/OgqN+cM48/Qaeb6AimYmZiUJmM9FFcmS3Q2n1k6/2OU3l3Hlin7bFpw==",
"cpu": [
"x64"
],
@@ -3092,9 +3096,9 @@
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.102.0.tgz",
"integrity": "sha512-txaURnQp3L/5mGgSkg6zFrI/H1BgwPxH+7fv/HMuFT1fTXGi7i5++OJNCFVPL7gFB50WFmpf02bCvgec3/gxMA==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.103.1.tgz",
"integrity": "sha512-rlaBeCul8pLbDRKdBAxdDxSBwFU5fbX42ci6CxW2Vbs34jWTMQ72+nE2v2KkGGce+JnYbb7UV6xDP/4uDWWi+w==",
"cpu": [
"arm64"
],
@@ -3109,9 +3113,9 @@
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.102.0.tgz",
"integrity": "sha512-KXs/y/bCVTxRy6NbgC2kK2JwmYnu4+EcDu+DVpHHIfiRgClTca++jYEqcxkthqK9ufkCgwMmrhb3tllyw5l1dw==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.103.1.tgz",
"integrity": "sha512-VSKFfhI//AOct6ppFmAaSH7tlW2LOFECMGs77jIoWf0ZjvGch/3sc9lUTTdIaYIOnTS4qpZck+8OcI2D0G92QA==",
"cpu": [
"x64"
],
@@ -3126,9 +3130,9 @@
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.102.0.tgz",
"integrity": "sha512-1nqC2lrn91fjEsf6gSr/KigU15FrhUSDGWdSWowLXvNG8KbYUNRKjfypn+8u4P0SrL+R/v4VSVZguZiVVTI94A==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.103.1.tgz",
"integrity": "sha512-tJRLPUtBwXHTnBnG7I39gQvCLreuOHLLCMyKHPR1hQ7aFNwiZjADYzKMRQPNMUqti/Os9z+6I7V13FqgQHfuzg==",
"cpu": [
"arm"
],
@@ -3144,9 +3148,9 @@
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.102.0.tgz",
"integrity": "sha512-Yg3Ga1deV+igVqjyK5gHMKbC6fdtKkVYBnzkPVbn/W2/DeActR2JFBfMwVtZJdLpNneMye3dRzOVO2QFegsTbQ==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.103.1.tgz",
"integrity": "sha512-rC+80/Xr9svo0ka2Zr/sjD+N1zEHTw2Urm/nsEjaVp5HSH2i22jwhTKTIqie2OLl6ti+qdYnbI0sSfxOYRtgrg==",
"cpu": [
"arm64"
],
@@ -3162,9 +3166,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.102.0.tgz",
"integrity": "sha512-WULwlrLbhHsJzGBcfztoXK47VyJugcWDmLXF893+yrMj6dF4/zJP6JqootT3EmAx9JAUdAppXI6ma7aGlnV8jw==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.103.1.tgz",
"integrity": "sha512-N8L/kgVzXpnH+8d6ErIzqvb0l8kHODR3OfPaf0Azw6DOLGtEjwDRUbHno+G7iogGzyPOjKsM8OdyqHJj/bpTdw==",
"cpu": [
"arm"
],
@@ -3180,9 +3184,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.102.0.tgz",
"integrity": "sha512-UFvRNJcYFOqo0X525vXG3Buue+waoqiG58xx7J6tMHSj5K/38t+9YbbgppKGLRoKoAwZTIdW8pxPA8qBSzQC6Q==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.103.1.tgz",
"integrity": "sha512-jMgG/C/VMo7+ShMFdk0xjJmQmYkj1PbJu/yrQiaQHpuo5pE2G+vnkEgsuM5EM3lTQ4MbMfnf715qy1fkbNPiUQ==",
"cpu": [
"arm64"
],
@@ -3198,9 +3202,9 @@
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.102.0.tgz",
"integrity": "sha512-7EmAMt5cX0SEN/qTNHD4VBHyjJXmFMAGuNR9R4YJR/SWGj/OZJw9VWryMfzEnMgNbG6HykiIttGfr9CyNKmZbA==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.103.1.tgz",
"integrity": "sha512-LZm8rEvI6sKU87qPOlODsYeAs7CmWy0B4ShYkjG4OMcGCcV4Ffxn25XW4TFqmruThITQvJ8WT+WGblzdoxYgbA==",
"cpu": [
"riscv64"
],
@@ -3216,9 +3220,9 @@
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.102.0.tgz",
"integrity": "sha512-HHzU5/g/MT1edbHfDzZleKcX19l6LMGk0KhZFLy66cxQl1iRLUaL22jfGstmHOdRUmVzWF3VevjSflSGvAjr4Q==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.103.1.tgz",
"integrity": "sha512-rmKzgk4t6RpaDhSefbbnk5yrA4pk1nHlVhJECGay8yMfzK8eglATeY9fzPgt89xCuQk6rkOiy11e+m8tLYOW5Q==",
"cpu": [
"x64"
],
@@ -3234,9 +3238,9 @@
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.102.0.tgz",
"integrity": "sha512-AUc4wHQ7IUo9/0mcZQn7Xkl1t1+NZlTF+v5i3GX93HNoQUGIFic7q6qjSERLkdwQ0dLnWrMqe8KhV98V8v0ZoA==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.103.1.tgz",
"integrity": "sha512-xydQmtxla31uMrmN6z+mAogVGdEMDmkUElVq2+udomX/TPZDYJBkApEyJl237M2VLWVv29v1N4U4i2Z4xa7SDA==",
"cpu": [
"riscv64"
],
@@ -3252,9 +3256,9 @@
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.102.0.tgz",
"integrity": "sha512-CxfhMwwbQFBCTjGysByvOw4PIOCYZ/jtz9uBU0UR5sfHtQvqqnsNyb1fVCIUsZ7piimxQAwcLV5H3Skj96QiVg==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.103.1.tgz",
"integrity": "sha512-Rln1oWm0MWKzzemOgQWrRFzfVBARwS9qxAidZPpCQHSohzPnWizFkmY1CUJad/YJNsA7sscueMdX63OEvxoM8w==",
"cpu": [
"x64"
],
@@ -3270,9 +3274,9 @@
}
},
"node_modules/sass-embedded-unknown-all": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.102.0.tgz",
"integrity": "sha512-qhqfvFD/7X0c6e5rwvzABkGlaYJju1G+hDPCiB0HdK+S7JF+YDDb9XpRYJ9+tGIopWzEHhoNnJDPtPs9IX1lOQ==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.103.1.tgz",
"integrity": "sha512-p7kI4US8+n+YguKczXh+4KcodJnk+8cwv+QFdvMaz3OfPrdHV0EnhPsaVDWD9qXb1XXCEdUhpp6vQwGk3eKc3A==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -3283,13 +3287,13 @@
"!win32"
],
"dependencies": {
"sass": "1.102.0"
"sass": "1.103.1"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.102.0.tgz",
"integrity": "sha512-EQOTKStMf7ervyhe3AX10Pa6F7CQMN3IgmpBkfgjy9HQG4Iyt34zCl/4vNJLVBWLW03b7/KJacy2I9SRz26X9g==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.103.1.tgz",
"integrity": "sha512-/0renOj3SpZGu4TA8CZJ6qZbTxGZk1khkPfiBTmV1uFB40ESYM5VsdXNj76P07bsAjIj9X2fCjTafLQFLSd9Nw==",
"cpu": [
"arm64"
],
@@ -3304,9 +3308,9 @@
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.102.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.102.0.tgz",
"integrity": "sha512-e+26mgR+O4g/m7r+WGuvygsjgZEsfgWntIOgaNhkSa9b7vVas70l2RB/SipUM4zGXQk1jX06aOEz0w0Pq1pprQ==",
"version": "1.103.1",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.103.1.tgz",
"integrity": "sha512-fIg60j9u5YlLUU3W8FvySMR4HHIo9R12HHFQTW7njyQ8nXqgR+t+XfCzGJloQka8g583BlAfu31Q76Ljr5o+9g==",
"cpu": [
"x64"
],
@@ -3578,16 +3582,16 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
"postcss": "^8.5.25",
"rolldown": "~1.2.1",
"postcss": "^8.5.26",
"rolldown": "~1.2.4",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -3604,7 +3608,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.4.0",
"@vitejs/devtools": "^0.4.0 || ^0.5.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -3656,19 +3660,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
"integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.10",
"@vitest/mocker": "4.1.10",
"@vitest/pretty-format": "4.1.10",
"@vitest/runner": "4.1.10",
"@vitest/snapshot": "4.1.10",
"@vitest/spy": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/expect": "4.1.11",
"@vitest/mocker": "4.1.11",
"@vitest/pretty-format": "4.1.11",
"@vitest/runner": "4.1.11",
"@vitest/snapshot": "4.1.11",
"@vitest/spy": "4.1.11",
"@vitest/utils": "4.1.11",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -3696,12 +3700,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.10",
"@vitest/browser-preview": "4.1.10",
"@vitest/browser-webdriverio": "4.1.10",
"@vitest/coverage-istanbul": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"@vitest/ui": "4.1.10",
"@vitest/browser-playwright": "4.1.11",
"@vitest/browser-preview": "4.1.11",
"@vitest/browser-webdriverio": "4.1.11",
"@vitest/coverage-istanbul": "4.1.11",
"@vitest/coverage-v8": "4.1.11",
"@vitest/ui": "4.1.11",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"

View File

@@ -23,15 +23,15 @@
"devDependencies": {
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.4",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"@vitest/ui": "^4.1.10",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"jsdom": "^30.0.1",
"oxlint": "^1.78.0",
"sass-embedded": "^1.102.0",
"vite": "^8.2.1",
"vitest": "^4.1.10"
"oxlint": "^1.79.0",
"sass-embedded": "^1.103.1",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
}

View File

@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
const ListTodo = (props) => {
// const { } = props;
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// AbortController instanziieren (seit 2019)
const controller = new AbortController();
setIsLoading(true);
setError(null);
fetch('https://dummyjson.com/todos/random/6', {
signal: controller.signal, // signal mitsenden
})
.then((res) => {
if (!res.ok) {
throw new Error('Http-Request went wrong');
}
return res.json();
})
.then((data) => {
setTodos(data);
})
.catch((err) => {
console.error(err);
if (err.name === 'AbortError') {
console.error('Fetch wurde abgebrochen');
} else if (err instanceof Error) {
console.error(err.message);
setError(err);
}
})
.finally(() => {
if (!controller.signal.aborted) {
setIsLoading(false);
}
});
return () => {
// unmount
controller.abort(); // The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.
};
}, []);
return (
<>
{!isLoading && (
<ul className="list-todo list-group">
{todos.length > 0 &&
todos.map(({ id, todo, completed, userId }, idx) => (
<li key={`list-item-todo-${id}`} className="list-group-item list-item-todo">
<input type="checkbox" checked={completed} name="todo-cb" id={`input-todo-${id}`} readOnly /> {todo}
</li>
))}
</ul>
)}
{error && <div className="alert alert-danger">{error?.message}</div>}
</>
);
};
export default ListTodo;

View File

@@ -0,0 +1,69 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
const ListUser = (props) => {
// const { } = props;
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 2019
setIsLoading(true);
axios
.get('https://dummyjson.com/users?limit=100', {
signal: controller.signal,
})
.then(({ data }) => {
console.log(data);
setUsers(data?.users);
})
.catch((err) => {
//
// if (err.name === 'AbortError') {}
if (axios.isCancel(err)) {
console.error('Request abgebrochen:', err.message);
} else {
console.error('Error:', err);
}
})
.finally(() => {
// Nur wenn die Komponente nicht abgebrochen/unmounted wurde,
// den Lade-Zustand auf false setzen
if (!controller.signal.aborted) {
setIsLoading(false);
}
});
return () => {
// umount
controller.abort();
};
}, []);
if (isLoading) {
return <p className="text-muted alert alert-secondary">is loading users...</p>;
}
return (
<ul className="list-todo list-group">
{users.length > 0 &&
users.map(({ id, firstName, lastName, age, email }, idx) => (
<li key={`list-item-user-${id}`} className="list-group-item list-item-user">
<p>
<strong>Name:</strong>
{firstName} {lastName}
<br />
<strong>age:</strong> {age}
<br />
<strong>email:</strong> {email}
<br />
</p>
</li>
))}
</ul>
);
};
export default ListUser;

View File

@@ -0,0 +1 @@
# JS Debug von Tag 46 - 50 absolviert

Binary file not shown.

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}

View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>01_react-app-with-tests</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
{
"name": "01_react-app-with-tests",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "oxlint",
"preview": "vite preview",
"test:ui": "npx vitest --ui",
"test": "vitest",
"test:watch": "vitest --watch"
},
"dependencies": {
"axios": "^1.19.0",
"bootstrap": "^5.3.8",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-icons": "^5.7.0",
"validator": "^13.15.35"
},
"devDependencies": {
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"jsdom": "^30.0.1",
"oxlint": "^1.79.0",
"sass-embedded": "^1.103.1",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,20 @@
import Button from './components/buttons/Button';
import ExampleComponent from './components/examples/ExampleComponent';
import FormContact from './components/forms/FormContact';
function App(props) {
// const { } = props;
return (
<>
<div className="container py-5">
<ExampleComponent />
<hr />
<Button />
<hr />
<FormContact />
</div>
</>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,15 @@
const Button = (props) => {
// const { } = props;
const handleClick = (e) => {
const btn = e.currentTarget;
btn.textContent = 'Clicked';
};
return (
<button className="btn btn-dark button-click" onClick={handleClick}>
Click Me!
</button>
);
};
export default Button;

View File

@@ -0,0 +1,10 @@
const ExampleComponent = (props) => {
// const { } = props;
return (
<>
<p>Hello World!</p>
</>
);
};
export default ExampleComponent;

View File

@@ -0,0 +1,18 @@
import { render, screen } from '@testing-library/react';
import ExampleComponent from '../ExampleComponent';
// import nicht notwendig ( Findet automatisch durch globale Setting statt)
// import { test, expect } from 'vitest';
// Test Case
test('Components renders correctly and contains text "Hello World"', () => {
// AAA
// Arrange, Acting, Assert
render(<ExampleComponent />);
const element = screen.getByText(/Hello World/i);
expect(element).toBeInTheDocument();
});
// console.log(document.querySelector('p').textContent.includes('Hello World')); // => true

View File

@@ -0,0 +1,167 @@
import { useState } from 'react';
import validate from '../../utils/validators/validate';
const FormContact = (props) => {
// const { } = props;
const [formData, setFormData] = useState({
firstname: '', //
lastname: '',
email: '',
message: '',
});
const [errors, setErrors] = useState({});
const [successMessage, setSuccessMessage] = useState('');
// EventHandlers =======
const handleSubmit = (e) => {
e.preventDefault(); // Standardverhalten (Versenden vom Formular) unterbinden.
const form = e.currentTarget;
const errors = validate(formData);
if (Object.keys(errors).length === 0) {
console.log('Form Data', formData);
// TODO: send over AJAX/Fetch
setSuccessMessage('Form submitted');
form.reset();
setFormData({
firstname: '', //
lastname: '',
email: '',
message: '',
});
setErrors({});
} else {
console.warn('Form submit error');
setErrors(errors);
setSuccessMessage('');
}
};
const handleBlur = (e) => {
const { name } = e.target;
const error = validate(formData);
if (error[name]) {
setErrors({ ...errors, [name]: error[name] });
} else {
setErrors({ ...errors, [name]: '' });
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((state) => ({ ...state, [name]: value }));
// console.log({ [e.target.name]: e.target.value });
// console.log('Formdata', formData);
};
return (
<form className="m-form-contact form-contact" noValidate onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-md">
<div className="mb-3">
<label htmlFor="input-firstname" className="form-label">
Firstname
</label>
<input
type="text"
name="firstname"
id="input-firstname"
className={`form-control input-firstname ${errors.firstname ? 'is-invalid' : ''}`}
value={formData.firstname}
onChange={handleChange}
onBlur={handleBlur}
/>
<div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.firstname || 'Error'}</div>
</div>
</div>
<div className="col-12 col-md">
<div className="mb-3">
<label htmlFor="input-lastname" className="form-label">
Lastname
</label>
<input
type="text"
name="lastname"
id="input-lastname"
className={`form-control input-lastname ${errors.lastname ? 'is-invalid' : ''}`}
value={formData.lastname}
onChange={handleChange}
onBlur={handleBlur}
/>
<div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.lastname}</div>
</div>
</div>
</div>
<div className="row">
<div className="col">
<div className="mb-3">
<label htmlFor="input-email" className="form-label">
E-Mail
</label>
<input
type="email"
name="email"
id="input-email"
className={`form-control input-email ${errors.email ? 'is-invalid' : ''}`}
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
/>
<div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.email}</div>
</div>
</div>
</div>
<div className="row">
<div className="col">
<div className="mb-3">
<label htmlFor="textarea-message" className="form-label">
Message
</label>
<textarea
name="message"
cols={4}
rows={6}
id="textarea-message"
className={`form-control textarea-message ${errors.message ? 'is-invalid' : ''}`}
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}></textarea>
<div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.message}</div>
</div>
</div>
</div>
<div className="row">
<div className="col">
<div className="mb-3">
<button type="submit" className="btn btn-dark button-form-send">
Send Message
</button>
</div>
</div>
</div>
{successMessage && (
<div data-testid="success-message" role="note" className="text-success alert alert-success">
{successMessage}
</div>
)}
</form>
);
};
export default FormContact;

View File

@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
const ListTodo = (props) => {
// const { } = props;
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// AbortController instanziieren (seit 2019)
const controller = new AbortController();
setIsLoading(true);
setError(null);
fetch('https://dummyjson.com/todos/random/6', {
signal: controller.signal, // signal mitsenden
})
.then((res) => {
if (!res.ok) {
throw new Error('Http-Request went wrong');
}
return res.json();
})
.then((data) => {
setTodos(data);
})
.catch((err) => {
console.error(err);
if (err.name === 'AbortError') {
console.error('Fetch wurde abgebrochen');
} else if (err instanceof Error) {
console.error(err.message);
setError(err);
}
})
.finally(() => {
if (!controller.signal.aborted) {
setIsLoading(false);
}
});
return () => {
// unmount
controller.abort(); // The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.
};
}, []);
return (
<>
{!isLoading && (
<ul className="list-todo list-group">
{todos.length > 0 &&
todos.map(({ id, todo, completed, userId }, idx) => (
<li key={`list-item-todo-${id}`} className="list-group-item list-item-todo">
<input type="checkbox" checked={completed} name="todo-cb" id={`input-todo-${id}`} readOnly /> {todo}
</li>
))}
</ul>
)}
{error && <div className="alert alert-danger">{error?.message}</div>}
</>
);
};
export default ListTodo;

View File

@@ -0,0 +1,69 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
const ListUser = (props) => {
// const { } = props;
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 2019
setIsLoading(true);
axios
.get('https://dummyjson.com/users?limit=100', {
signal: controller.signal,
})
.then(({ data }) => {
console.log(data);
setUsers(data?.users);
})
.catch((err) => {
//
// if (err.name === 'AbortError') {}
if (axios.isCancel(err)) {
console.error('Request abgebrochen:', err.message);
} else {
console.error('Error:', err);
}
})
.finally(() => {
// Nur wenn die Komponente nicht abgebrochen/unmounted wurde,
// den Lade-Zustand auf false setzen
if (!controller.signal.aborted) {
setIsLoading(false);
}
});
return () => {
// umount
controller.abort();
};
}, []);
if (isLoading) {
return <p className="text-muted alert alert-secondary">is loading users...</p>;
}
return (
<ul className="list-todo list-group">
{users.length > 0 &&
users.map(({ id, firstName, lastName, age, email }, idx) => (
<li key={`list-item-user-${id}`} className="list-group-item list-item-user">
<p>
<strong>Name:</strong>
{firstName} {lastName}
<br />
<strong>age:</strong> {age}
<br />
<strong>email:</strong> {email}
<br />
</p>
</li>
))}
</ul>
);
};
export default ListUser;

View File

@@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'bootstrap'; // import von JS
import 'bootstrap/dist/css/bootstrap.min.css'; // CSS import
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,19 @@
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../components/buttons/Button';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest'; // Muss nicht eingebunden werden. Einbindung findet automatisch statt, allerdings haben wir dann keine Autocomplete Funktion
// Test Case
test('Button is clickable and changes text on click to "Clicked"', async () => {
render(<Button />);
const user = userEvent.setup();
const button = screen.getByRole('button');
// fireEvent.click(button);
await user.click(button);
expect(button).toHaveTextContent(/Clicked/i);
});

View File

@@ -0,0 +1,18 @@
import { render, screen } from '@testing-library/react';
import ExampleComponent from '../components/examples/ExampleComponent';
// import nicht notwendig ( Findet automatisch durch globale Setting statt)
import { test, expect } from 'vitest';
// Test Case
test('Components renders correctly and contains text "Hello World"', () => {
// AAA
// Arrange, Acting, Assert
render(<ExampleComponent />);
const element = screen.getByText(/Hello World/i);
expect(element).toBeInTheDocument();
});
// console.log(document.querySelector('p').textContent.includes('Hello World')); // => true

View File

@@ -0,0 +1,409 @@
import { render, screen } from '@testing-library/react';
import FormContact from '../components/forms/FormContact';
import userEvent from '@testing-library/user-event';
import { expect, vi, afterAll, describe, test } from 'vitest';
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); //
afterAll(() => {
consoleSpy.mockRestore();
});
// Test Suite
describe('FormContact', () => {
// Test Case
test('FormContact submits data object correctly', async () => {
render(<FormContact />);
const user = userEvent.setup();
// find the input
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john.wick@continentalhotel.com');
await user.type(messageInput, 'Hello, this is a test message');
await user.click(submitButton);
// const succesMessageDiv = screen.getByTestId('success-message'); // data-testid Attribut muss gesetzt werden
// const succesMessageDiv = screen.getByRole('note', { name: /Form submitted/i }); // data-testid Attribut muss gesetzt werden
// const succesMessageDiv = screen.getByText(/Form submitted/i); // data-testid Attribut muss gesetzt werden
// expect(succesMessageDiv).toHaveTextContent(/Form submitted/i);
expect(consoleSpy).toHaveBeenCalledWith('Form Data', {
firstname: 'John',
lastname: 'Wick',
email: 'john.wick@continentalhotel.com',
message: 'Hello, this is a test message',
});
});
// ===== FIRSTNAME FIELD TESTS =====
describe('Firstname field error messages', () => {
test('shows error message when firstname is empty on submit', async () => {
render(<FormContact />);
const user = userEvent.setup();
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
await user.click(submitButton);
expect(screen.getByText('Vorname ist erforderlich.')).toBeInTheDocument();
});
test('shows error message when firstname is too short (less than 2 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(firstNameInput, 'J');
await user.click(submitButton);
expect(
screen.getByText('Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('shows error message when firstname is too long (more than 60 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
const longName = 'a'.repeat(61);
await user.click(firstNameInput);
await user.paste(longName);
await user.click(submitButton);
expect(
screen.getByText('Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('no error message when firstname is valid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
// Trigger onBlur to validate
await user.tab();
expect(firstNameInput).not.toHaveClass('is-invalid');
});
});
// ===== LASTNAME FIELD TESTS =====
describe('Lastname field error messages', () => {
test('shows error message when lastname is empty on submit', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(firstNameInput, 'John');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
await user.click(submitButton);
expect(screen.getByText('Nachname ist erforderlich.')).toBeInTheDocument();
});
test('shows error message when lastname is too short (less than 2 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const lastNameInput = screen.getByLabelText('Lastname');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(lastNameInput, 'W');
await user.click(submitButton);
expect(
screen.getByText('Nachname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('shows error message when lastname is too long (more than 60 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const lastNameInput = screen.getByLabelText('Lastname');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
const longName = 'b'.repeat(61);
await user.click(lastNameInput);
await user.paste(longName);
await user.click(submitButton);
expect(
screen.getByText('Nachname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('no error message when lastname is valid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
// Trigger onBlur to validate
await user.tab();
expect(lastNameInput).not.toHaveClass('is-invalid');
});
});
// ===== EMAIL FIELD TESTS =====
describe('Email field error messages', () => {
test('shows error message when email is empty on submit', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(messageInput, 'This is a valid test message');
await user.click(submitButton);
expect(screen.getByText('E-Mail ist erforderlich.')).toBeInTheDocument();
});
test('shows error message when email format is invalid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const emailInput = screen.getByLabelText('E-Mail');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(emailInput, 'invalidemail');
await user.click(submitButton);
expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
});
test('shows error message when email is missing @ symbol', async () => {
render(<FormContact />);
const user = userEvent.setup();
const emailInput = screen.getByLabelText('E-Mail');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(emailInput, 'johnexample.com');
await user.click(submitButton);
expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
});
test('shows error message when email is missing domain', async () => {
render(<FormContact />);
const user = userEvent.setup();
const emailInput = screen.getByLabelText('E-Mail');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(emailInput, 'john@');
await user.click(submitButton);
expect(screen.getByText('Bitte gib eine gültige E-Mail-Adresse ein.')).toBeInTheDocument();
});
test('no error message when email is valid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
// Trigger onBlur to validate
await user.tab();
expect(emailInput).not.toHaveClass('is-invalid');
});
});
// ===== MESSAGE FIELD TESTS =====
describe('Message field error messages', () => {
test('shows error message when message is empty on submit', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.click(submitButton);
expect(screen.getByText('Nachricht ist erforderlich.')).toBeInTheDocument();
});
test('shows error message when message is too short (less than 10 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(messageInput, 'short msg');
await user.click(submitButton);
expect(
screen.getByText('Die Nachricht muss mindestens 10 und maximal 1000 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('shows error message when message is too long (more than 1000 chars)', async () => {
render(<FormContact />);
const user = userEvent.setup();
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
const longMessage = 'a'.repeat(1001);
await user.click(messageInput);
await user.paste(longMessage);
// await user.type(messageInput, longMessage); //. 10001 mal rerendern von FormContact. (Performance issues)
await user.click(submitButton);
expect(
screen.getByText('Die Nachricht muss mindestens 10 und maximal 1000 Zeichen lang sein.'),
).toBeInTheDocument();
});
test('no error message when message is valid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
const emailInput = screen.getByLabelText('E-Mail');
const messageInput = screen.getByLabelText('Message');
await user.type(firstNameInput, 'John');
await user.type(lastNameInput, 'Wick');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'This is a valid test message');
// Trigger onBlur to validate
await user.tab();
expect(messageInput).not.toHaveClass('is-invalid');
});
});
// ===== ONBLUR VALIDATION TESTS =====
describe('Form field onBlur validation', () => {
test('shows error on firstname field after blur when invalid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
await user.type(firstNameInput, 'J');
await user.click(screen.getByLabelText('Lastname')); // Trigger blur on firstname
expect(firstNameInput).toHaveClass('is-invalid');
});
test('clears error on firstname field after blur when valid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const firstNameInput = screen.getByLabelText('Firstname');
const lastNameInput = screen.getByLabelText('Lastname');
await user.type(firstNameInput, 'J');
await user.click(lastNameInput); // Trigger blur with invalid input
expect(firstNameInput).toHaveClass('is-invalid');
// Clear and type valid input
await user.clear(firstNameInput);
await user.type(firstNameInput, 'John');
await user.click(screen.getByLabelText('E-Mail')); // Trigger blur with valid input
expect(firstNameInput).not.toHaveClass('is-invalid');
});
test('shows error on email field after blur when invalid', async () => {
render(<FormContact />);
const user = userEvent.setup();
const emailInput = screen.getByLabelText('E-Mail');
await user.type(emailInput, 'invalidemail');
await user.click(screen.getByLabelText('Message')); // Trigger blur on email
expect(emailInput).toHaveClass('is-invalid');
});
test('shows error on message field after blur when too short', async () => {
render(<FormContact />);
const user = userEvent.setup();
const messageInput = screen.getByLabelText('Message');
const submitButton = screen.getByRole('button', { name: 'Send Message' });
await user.type(messageInput, 'short');
await user.click(submitButton); // Trigger blur on message
expect(messageInput).toHaveClass('is-invalid');
});
});
});

View File

@@ -0,0 +1,14 @@
// src/test/setup.js
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
// add jest-dom matchers to expect
expect.extend(matchers);
// Add cleanup function to run after each test
afterEach(() => {
cleanup();
});
// More setup code here, if needed

View File

@@ -0,0 +1,37 @@
import validator from 'validator';
const validate = (formData) => {
const errors = {};
// Firstname min 2 chars
if (validator.isEmpty(formData.firstname.trim())) {
errors.firstname = 'Vorname ist erforderlich.';
} else if (!validator.isLength(formData.firstname.trim(), { min: 2, max: 60 })) {
errors.firstname = 'Vorname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.';
}
// Lastname
if (validator.isEmpty(formData.lastname.trim())) {
errors.lastname = 'Nachname ist erforderlich.';
} else if (!validator.isLength(formData.lastname.trim(), { min: 2, max: 60 })) {
errors.lastname = 'Nachname muss mindestens 2 Zeichen und maximal 60 Zeichen lang sein.';
}
// Email
if (validator.isEmpty(formData.email.trim())) {
errors.email = 'E-Mail ist erforderlich.';
} else if (!validator.isEmail(formData.email.trim())) {
errors.email = 'Bitte gib eine gültige E-Mail-Adresse ein.';
}
// Message
if (validator.isEmpty(formData.message.trim())) {
errors.message = 'Nachricht ist erforderlich.';
} else if (!validator.isLength(formData.message.trim(), { min: 10, max: 1000 })) {
errors.message = 'Die Nachricht muss mindestens 10 und maximal 1000 Zeichen lang sein.';
}
return errors;
};
export default validate;

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
test: {
// Vitest configuration
globals: true, // make the global objects available in the test environment
environment: 'jsdom', // use jsdom as the test environment
setupFiles: './src/test/setup.js', // if you have a setup file
// more options here
},
});

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,132 @@
# Assignment: Completing the Multi-Step Wizard Form
In this assignment, you will complete a multi-step wizard form by adding two final steps, Step 4 (Personal Preferences) and Step 5 (Ready to Start), bringing the total number of steps to five. Your tasks include:
- Fixing any failing components so that all existing tests pass.
- Adding two new steps with validation logic.
- Writing tests to ensure the new steps are functional and robust.
Below are detailed requirements for the two new steps and their associated tests.
## Adding Step 4 and Step 5 to the Multi-Step Wizard Form
This section outlines the requirements for implementing Step 4: Personal Preferences and Step 5: Ready to Start. Your implementation should maintain consistency with the existing form design and functionality while ensuring robust validation and comprehensive test coverage.
### Step 4: Personal Preferences
This step gathers information about the users preferred activities and dietary preferences.
#### Inputs
- **Checkboxes**: Allow users to select one or more activities. Options include:
- Yoga
- Cardio
- Strength Training
- Meditation
- **Dropdown (Select)**: Allow users to specify their dietary preferences. Options include:
- None
- Vegetarian
- Vegan
- Gluten-Free
Display the following text in this step: “Let us know your preferences to make the experience personal.”
#### What to Test
1. Rendering of Form Fields:
- **Activity Checkboxes**:
- Verify that checkboxes for activities are rendered and grouped under the label “What type of activities do you prefer?”
- Ensure that all activity options (Yoga, Cardio, Strength Training, Meditation) are present and selectable.
- **Dietary Preferences Dropdown**:
- Confirm that the dropdown for dietary preferences is rendered with the label “Do you have any dietary preferences?”
- Ensure that all dietary options (None, Vegetarian, Vegan, Gluten-Free) are available for selection.
2. Validation of User Input:
- **Activity Preferences Validation**:
- Attempt to proceed without selecting any activities and verify that an error message appears stating “At least one activity preference is required.”
- Select one or more activities and ensure that the error message disappears upon valid input.
- **Dietary Preferences Validation**:
- Attempt to proceed without selecting a dietary preference and verify that an error message appears stating “Dietary preference is required.”
- Select a dietary preference and ensure that the error message disappears upon valid input.
3. Dynamic Updates to Form Data:
- **Activity Checkboxes Interaction**:
- Select and deselect activity checkboxes and confirm that the activityPreferences array in the form data updates accordingly.
- **Dietary Preferences Dropdown Interaction**:
- Change the selection in the dietary preferences dropdown and verify that the diet field in the form data updates to reflect the current selection.
#### Test Summary for Step 4: Personal Preferences
- **Test Case 1**: Verifies that all form fields (activity checkboxes and dietary preferences dropdown) are rendered correctly with the appropriate labels and options.
- **Test Case 2**: Ensures validation errors appear when no activities or dietary preferences are selected and disappear when valid inputs are provided.
- **Test Case 3**: Confirms that interacting with the form elements (selecting activities and dietary preferences) dynamically updates the form data.
### Step 5: Ready to Start
This step collects motivational information, enables users to join optional challenges, and allows them to choose their preferred reminder method.
#### Inputs
- **Short Text Input**: A field for users to describe what motivates them the most. Placeholder: “What motivates you the most?”
- **Checkboxes**: Options to join one or more optional challenges, including:
- Fitness Challenge
- Healthy Eating Challenge
- Mindfulness Challenge
- **Dropdown (Select)**: A field to specify how users want to receive reminders. Options include:
- Email
- Push Notifications
- None
Display the following text in this step: “We are ready to guide you! Set your preferences and begin your journey.”
#### What to Test
1. Rendering of Form Fields:
- **Motivational Input Field**:
- Verify that the motivational input field is rendered with the label “What motivates you the most?” and the placeholder text is correctly displayed.
- **Optional Challenges Checkboxes**:
- Confirm that checkboxes for optional challenges are rendered and grouped under the label “Would you like to join any optional challenges?”
- Ensure that all challenge options (Fitness Challenge, Healthy Eating Challenge, Mindfulness Challenge) are present and selectable.
- **Reminders Dropdown**:
- Check that the dropdown for reminder preferences is rendered with the label “Preferred way to receive reminders?”
- Ensure that all reminder options (Email, Push Notifications, None) are available for selection.
2. Validation and Dynamic Updates of User Input:
- **Form Validation**:
- Enter valid inputs for all fields:
- Type a non-empty string into the motivation field.
- Optionally select one or more challenges (since they are optional, the form should accept both selecting and not selecting any challenges).
- Select a valid option from the reminders dropdown.
- Ensure that the form passes validation without any errors when all inputs are valid.
- **Dynamic Updates to Form Data**:
- Confirm that typing into the motivation field updates the motivation field in the form data.
- Verify that selecting or deselecting challenges updates the optionalChallenges array in the form data.
- Ensure that changing the selection in the reminders dropdown updates the reminders field in the form data.
#### Test Summary for Step 5: Ready to Start
- **Test Case 1**: Verifies that all form fields (motivation input, optional challenges checkboxes, and reminders dropdown) are rendered correctly with the appropriate labels and options.
- **Test Case 2**: Ensures the form passes validation with valid inputs and confirms that user interactions (typing in the motivation field, selecting/deselecting challenges, choosing a reminder option) dynamically update the form data accurately.
## License and terms of use
© Web Professional Institute Inc. All rights reserved.
This material is provided solely for students enrolled in courses offered by Web Professional Institute Inc. By accessing or using this code, you acknowledge that it is strictly for educational use within the context of Web Professional Institute Inc. programs.
**Usage Restrictions:**
- Redistribution, sharing, or copying of this material outside the course environment is strictly prohibited.
- The content is designed to support your learning objectives and is not authorized for commercial projects, public repositories, or applications beyond the course scope.
- Unauthorized commercial use or open-source distribution is strictly prohibited and may result in expulsion from the program and legal action.
**Agreement & Rights:**
As a student, you have agreed to these terms as part of your enrollment agreement, which includes additional details on permitted uses, restrictions, and policies. The author and Web Professional Institute Inc. retain all rights to this material, including the code base and instructional content.
For any questions regarding these terms, please contact Web Professional Institute Inc. for clarification.

View File

@@ -0,0 +1,32 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "debugging-assignment-wizard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/forms": "^0.5.11",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"autoprefixer": "^10.5.4",
"eslint": "^10.9.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"globals": "^17.11.0",
"jsdom": "^30.0.1",
"postcss": "^8.5.26",
"tailwindcss": "^3.4.19",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}
}

View File

@@ -0,0 +1,9 @@
// #region agent log
fetch('http://127.0.0.1:7399/ingest/04244453-f432-4f3b-ade1-92bbdb574a0e',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'a04de5'},body:JSON.stringify({sessionId:'a04de5',runId:'post-fix',hypothesisId:'H3',location:'postcss.config.js',message:'postcss config loaded',data:{plugins:['tailwindcss','autoprefixer']},timestamp:Date.now()})}).catch(()=>{});
// #endregion
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,12 @@
import React from 'react';
import MultiStepForm from './MultiStepForm';
function App() {
return (
<div className="App">
<MultiStepForm />
</div>
);
}
export default App;

View File

@@ -0,0 +1,125 @@
import { useState } from 'react';
import ProgressIndicator from './components/ProgressIndicator';
import NavigationButtons from './components/NavigationButtons';
import AccountInfo from './steps/AccountInfo';
import ActivityInfo from './steps/ActivityInfo';
import GoalSetting from './steps/GoalSetting';
import { validate } from './utils/validation';
function MultiStepForm() {
const [stepIndex, setStepIndex] = useState(1);
const [errors, setErrors] = useState({});
const [formData, setFormData] = useState({
fullName: '',
email: '',
password: '',
activityLevel: 'never',
energyLevel: 'low',
goals: [],
timeCommitment: '15',
});
const formSteps = [
{ id: 1, name: 'Account Info', component: AccountInfo },
{ id: 2, name: 'Activity Info', component: ActivityInfo },
{ id: 3, name: 'Goal Setting', component: GoalSetting },
];
// ===== Event Handlers =====
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleCheckboxChange = (field, id, isChecked) => {
setFormData((prev) => ({
...prev,
[field]: isChecked
? [...prev[field], id]
: prev[field].filter((item) => item !== id),
}));
};
// ===== Form Navigation =====
const handleNext = () => {
if (handleValidation()) {
setStepIndex(stepIndex + 1);
}
};
const handlePrev = () => {
setErrors({});
setStepIndex(stepIndex - 1);
};
// ===== Form Validation & Submission =====
const handleValidation = () => {
const validationErrors = validate(formData, stepIndex);
setErrors(validationErrors);
return Object.keys(validationErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (handleValidation()) {
alert('Form submitted successfully!');
console.log('Submitted Data:', formData);
}
};
return (
<div className="min-h-screen bg-gray-100">
<div className="pt-32 mx-4">
<div className="overflow-hidden rounded-lg bg-white shadow max-w-[600px] mx-auto">
<div className="px-4 py-5 sm:p-6">
<h1 className="text-2xl font-bold text-center my-6">
Let's get you started!
</h1>
<ProgressIndicator steps={formSteps} currentStep={stepIndex} />
<form onSubmit={handleSubmit} className="relative mt-10">
{formSteps.map((s) => {
const StepComponent = s.component;
return (
<div
key={s.id}
className={`transition-opacity duration-500 ${
stepIndex === s.id
? 'opacity-100'
: 'opacity-0 absolute w-full'
}`}
aria-hidden={stepIndex !== s.id}
>
{stepIndex === s.id && (
<StepComponent
formData={formData}
handleChange={handleChange}
handleCheckboxChange={handleCheckboxChange}
errors={errors}
/>
)}
</div>
);
})}
{/* Navigation Buttons */}
<NavigationButtons
currentStep={stepIndex}
totalSteps={formSteps.length}
handlePrev={handlePrev}
handleNext={handleNext}
handleSubmit={handleSubmit}
/>
</form>
</div>
</div>
</div>
</div>
);
}
export default MultiStepForm;

View File

@@ -0,0 +1,44 @@
import React from 'react';
const FormCheckboxes = ({ legend, required, options, onChange, error }) => {
return (
<div>
<label className="block text-sm/6 font-medium text-gray-900">
{legend} {required && <span className="text-red-500">*</span>}
</label>
<fieldset className="mt-4">
<legend className="sr-only">{legend}</legend>
<div className="space-y-5">
{options.map((option) => (
<div key={option.id} className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id={option.id}
name={option.name}
type="checkbox"
aria-describedby={`${option.id}-description`}
className="size-4 rounded border-gray-300 text-teal-600 focus:ring-teal-600"
onChange={(e) => onChange(option.id, e.target.checked)}
checked={option.checked}
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor={option.id}
className="font-medium text-gray-900"
>
{option.label}
</label>
</div>
</div>
))}
</div>
</fieldset>
<p className="mt-2 text-sm text-red-600">{error}</p>
</div>
);
};
export default FormCheckboxes;

View File

@@ -0,0 +1,47 @@
import React from 'react';
const FormInput = ({
htmlFor,
label,
id,
name,
value,
onChange,
error,
placeholder,
required = false,
type = 'text',
}) => {
return (
<div>
<label
htmlFor={htmlFor}
className="block text-sm/6 font-medium text-gray-900"
>
{label} {required && <span className="text-red-500">*</span>}
</label>
<div className="mt-2">
<input
type={type}
id={id}
name={name}
value={value}
onChange={onChange}
className={`block w-full rounded-md border-0 py-1.5 px-2.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm/6 ${
error
? 'text-red-900 ring-red-400 placeholder:text-red-300 focus:ring-red-500'
: 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-teal-600'
}`}
placeholder={placeholder}
aria-required={required}
aria-invalid={error ? 'true' : 'false'}
/>
</div>
<p className="mt-2 text-sm text-red-600">{error}</p>
</div>
);
};
export default FormInput;

View File

@@ -0,0 +1,44 @@
import React from 'react';
const FormSelect = ({
id,
name,
options,
defaultValue,
onChange,
label,
required = false,
className = '',
labelClassName = '',
error,
}) => {
return (
<div>
{label && (
<label
htmlFor={id}
className={`block text-sm font-medium text-gray-900 ${labelClassName}`}
>
{label} {required && <span className="text-red-500">*</span>}
</label>
)}
<select
id={id}
name={name}
defaultValue={defaultValue}
onChange={onChange}
className={`mt-2 block w-full rounded-md border-0 py-2.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-teal-600 sm:text-sm ${className}`}
>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</select>
<p className="mt-2 text-sm text-red-600">{error}</p>
</div>
);
};
export default FormSelect;

View File

@@ -0,0 +1,43 @@
import React from 'react';
function NavigationButtons({
currentStep,
totalSteps,
handlePrev,
handleNext,
handleSubmit,
}) {
return (
<div className="flex justify-between mt-10">
{currentStep > 1 && (
<button
type="button"
onClick={handlePrev}
className="px-8 py-2.5 bg-gray-200 text-gray-700 text-sm font-semibold rounded-md hover:bg-gray-300 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
>
Previous
</button>
)}
{currentStep < totalSteps && (
<button
type="button"
onClick={handleNext}
className="ml-auto px-11 py-2.5 bg-teal-600 text-sm font-semibold text-white shadow-sm rounded-md hover:bg-teal-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"
>
Next
</button>
)}
{currentStep === totalSteps && (
<button
type="submit"
onClick={handleSubmit}
className="ml-auto px-10 py-2.5 bg-teal-600 text-sm font-semibold text-white shadow-sm rounded-md hover:bg-teal-700 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"
>
Submit
</button>
)}
</div>
);
}
export default NavigationButtons;

View File

@@ -0,0 +1,33 @@
import React from 'react';
function ProgressIndicator({ steps, currentStep }) {
return (
<div className="flex items-center justify-between mb-8">
{steps.map((step, index) => (
<div key={step.id} className="flex-1">
<div className="flex items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
currentStep >= step.id
? 'bg-teal-600 text-white'
: 'bg-gray-300 text-gray-700'
}`}
aria-current={currentStep === step.id ? 'step' : undefined}
>
{step.id}
</div>
{index !== steps.length - 1 && (
<div
className={`flex-1 h-1 ${
currentStep > step.id ? 'bg-teal-600' : 'bg-gray-300'
}`}
></div>
)}
</div>
</div>
))}
</div>
);
}
export default ProgressIndicator;

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,60 @@
import FormInput from '../components/FormInput';
function AccountInfo({ formData, handleChange, errors }) {
return (
<div>
<h2 className="text-xl font-semibold mb-4">
Welcome to BrightPath Wellness!
</h2>
<p className="mb-6 text-gray-700">
Lets create your account and begin your wellness journey.
</p>
<div className="mb-4">
<FormInput
htmlFor="fullName"
label="Full Name"
type="text"
id="fullName"
name="fullName"
value={''}
onChange={handleChange}
error={errors.fullName}
placeholder="Enter your full name"
required={true}
/>
</div>
<div className="mb-4">
<FormInput
htmlFor="email"
label="Email"
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
error={errors.email}
placeholder="Enter your email address"
required={true}
/>
</div>
<div className="mb-4">
<FormInput
htmlFor="password"
label="Password"
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Create a password"
required={true}
/>
</div>
</div>
);
}
export default AccountInfo;

View File

@@ -0,0 +1,45 @@
import FormSelect from '../components/FormSelect';
function ActivityInfo({ handleChange, errors }) {
return (
<div>
<h2 className="text-xl font-semibold mb-4">Activity & Energy</h2>
<p className="mb-6 text-gray-700">
Tell us a little about your activity level and energy.
</p>
<div className="mb-4">
<FormSelect
id="activityLevel"
name="activityLevel"
options={[
{ value: 'never', label: 'Never' },
{ value: 'rarely', label: 'Rarely' },
{ value: '1-2 times a week', label: '1-2 times a week' },
{ value: '3+ times a week', label: '3+ times a week' },
]}
onChange={handleChange}
required={true}
error={errors.activityLevel}
/>
</div>
<div className="mb-4">
<FormSelect
id="energyLevel"
name="energyLevel"
options={[
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
]}
onChange={handleChange}
required={true}
error={errors.energyLevel}
/>
</div>
</div>
);
}
export default ActivityInfo;

View File

@@ -0,0 +1,65 @@
import FormSelect from '../components/FormSelect';
import FormCheckboxes from '../components/FormCheckboxes';
function GoalSetting({ formData, handleChange, handleCheckboxChange, errors }) {
return (
<div>
<h2 className="text-xl font-semibold mb-4">Activity & Energy</h2>
<p className="mb-6 text-gray-700">
Tell us a little about your activity level and energy.
</p>
<div className="mb-10">
<FormCheckboxes
legend="What are your goals?"
options={[
{
id: 'weightLoss',
name: 'goals',
label: 'Weight Loss',
checked: formData.goals.includes('weightLoss'),
},
{
id: 'muscleGain',
name: 'goals',
label: 'Muscle Gain',
checked: formData.goals.includes('musleGain'),
},
{
id: 'improveHealth',
name: 'goals',
label: 'Improve Health',
checked: formData.goals.includes('improveHealt'),
},
{
id: 'boostEnergy',
name: 'goals',
label: 'Boost Energy',
checked: formData.goals.includes('boostEnergy'),
},
]}
onChange={(id, isChecked) => handleCheckboxChange('goals', id)}
required={true}
error={errors.goals}
/>
</div>
<div className="mb-4">
<FormSelect
id="timeCommitment"
name="timeCommitment"
options={[
{ value: '15', label: '15 minutes' },
{ value: '30', label: '30 minutes' },
{ value: '60', label: '1 hour' },
]}
onChange={handleChange}
label="How much time can you commit each day?"
required={true}
/>
</div>
</div>
);
}
export default GoalSetting;

View File

@@ -0,0 +1,96 @@
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const PASSWORD_MIN_LENGTH = 6;
// Helper Functions
export const isEmpty = (value) => {
return !value || !value.trim();
};
export const isValidEmail = (email) => {
return EMAIL_REGEX.test(email);
};
export const hasMinLength = (value, min) => {
return value.length >= min;
};
export const hasTwoWords = (fullName) => {
return fullName.trim().split(' ').length >= 2;
};
// Main Validation Function
export const validate = (formData, currentStep) => {
const errors = {};
const {
fullName,
email,
password,
activityLevel,
energyLevel,
goals,
timeCommitment,
} = formData;
switch (currentStep) {
case 1:
// Full Name Validations
if (isEmpty(fullName)) {
errors.fullName = 'Full Name is required';
} else if (!hasTwoWords(fullName)) {
errors.fullName = 'Full Name must contain at least two words';
}
// Email Validations
if (isEmpty(email)) {
errors.email = 'Email is required';
} else if (!isValidEmail(email)) {
errors.email = 'Email address is invalid';
}
// Password Validations
if (isEmpty(password)) {
errors.password = 'Password is required';
} else {
if (!hasMinLength(password, PASSWORD_MIN_LENGTH)) {
errors.password = `Password must be at least ${PASSWORD_MIN_LENGTH} characters`;
}
}
break;
case 2:
// Activity Level Validation
if (isEmpty(activityLevel)) {
errors.activityLevel = 'Activity Level is required';
}
// Energy Level Validation
if (isEmpty(energyLevel)) {
errors.energyLevel = 'Energy Level is required';
}
break;
case 3:
// Goals Validation
if (!Array.isArray(goals) || goals.length === 0) {
errors.goals = 'At least one goal is required';
} else {
goals.forEach((goal, index) => {
if (isEmpty(goal)) {
errors[`goals.${index}`] = 'Goal cannot be empty';
}
});
}
// Time Commitment Validation
if (isEmpty(timeCommitment)) {
errors.timeCommitment = 'Time Commitment is required';
}
break;
default:
break;
}
return errors;
};

View File

@@ -0,0 +1,10 @@
import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [forms],
};

View File

@@ -0,0 +1,125 @@
import { it, expect, describe, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import userEvent from '@testing-library/user-event';
import AccountInfo from '../../src/steps/AccountInfo';
import { validate } from '../../src/utils/validation';
describe('AccountInfo Component', () => {
afterEach(cleanup);
const step = 1;
it('renders all form fields', () => {
render(
<AccountInfo
formData={{ fullName: '', email: '', password: '' }}
handleChange={() => {}}
errors={{}}
/>
);
expect(screen.getByLabelText(/Full Name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Password/i)).toBeInTheDocument();
});
it('displays validation errors for invalid data', () => {
const formData = { fullName: '', email: '', password: '' };
const errors = validate(formData, step);
render(
<AccountInfo
formData={formData}
handleChange={() => {}}
errors={errors}
/>
);
expect(screen.getByText(/Full Name is required/i)).toBeInTheDocument();
expect(screen.getByText(/Email is required/i)).toBeInTheDocument();
expect(screen.getByText(/Password is required/i)).toBeInTheDocument();
});
it('displays no validation errors for valid data', () => {
const formData = {
fullName: 'John Doe',
email: 'john.doe@example.com',
password: 'securepassword',
};
const errors = validate(formData, step);
render(
<AccountInfo
formData={formData}
handleChange={() => {}}
errors={errors}
/>
);
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
expect(screen.queryByText(/invalid/i)).not.toBeInTheDocument();
expect(
screen.queryByText(/must be at least 6 characters/i)
).not.toBeInTheDocument();
});
it('calls handleChange and updates error messages dynamically', async () => {
let formData = { fullName: '', email: '', password: '' };
let errors = validate(formData, step);
const handleChangeTest = (field, value) => {
formData = { ...formData, [field]: value };
errors = validate(formData, step);
};
const { rerender } = render(
<AccountInfo
formData={formData}
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
errors={errors}
/>
);
const fullNameInput = screen.getByLabelText(/Full Name/i);
await userEvent.type(fullNameInput, 'John');
rerender(
<AccountInfo
formData={formData}
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
errors={errors}
/>
);
expect(
screen.getByText(/Full Name must contain at least two words/i)
).toBeInTheDocument();
});
it('retains user input in form fields', () => {
const formData = {
fullName: 'Jane Doe',
email: 'jane.doe@example.com',
password: 'securepassword',
};
const errors = validate(formData, step);
render(
<AccountInfo
formData={formData}
handleChange={() => {}}
errors={errors}
/>
);
expect(screen.getByLabelText(/Full Name/i)).toHaveValue('Jane Doe');
expect(screen.getByLabelText(/Email/i)).toHaveValue('jane.doe@example.com');
expect(screen.getByLabelText(/Password/i)).toHaveValue('securepassword');
});
});

View File

@@ -0,0 +1,114 @@
import { it, expect, describe, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import userEvent from '@testing-library/user-event';
import ActivityInfo from '../../src/steps/ActivityInfo';
import { validate } from '../../src/utils/validation';
describe('ActivityInfo Component', () => {
afterEach(cleanup);
const step = 2;
it('renders all form fields', () => {
render(<ActivityInfo handleChange={() => {}} errors={{}} />);
expect(
screen.getByLabelText(/How often do you exercise\?/i)
).toBeInTheDocument();
expect(
screen.getByLabelText(
/How would you describe your average energy level\?/i
)
).toBeInTheDocument();
});
it('displays validation errors for invalid data', () => {
const formData = {
fullName: 'John Doe',
email: 'john@example.com',
password: 'password123',
activityLevel: '',
energyLevel: '',
goals: [],
timeCommitment: '15',
};
const errors = validate(formData, step);
render(<ActivityInfo handleChange={() => {}} errors={errors} />);
expect(screen.getByText(/Activity Level is required/i)).toBeInTheDocument();
expect(screen.getByText(/Energy Level is required/i)).toBeInTheDocument();
});
it('displays no validation errors for valid data', () => {
const formData = {
fullName: 'John Doe',
email: 'john@example.com',
password: 'password123',
activityLevel: 'moderate',
energyLevel: 'high',
goals: [],
timeCommitment: '15',
};
const errors = validate(formData, step);
render(<ActivityInfo handleChange={() => {}} errors={errors} />);
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
});
it('calls handleChange and updates error messages dynamically', async () => {
let formData = {
fullName: 'John Doe',
email: 'john@example.com',
password: 'password123',
activityLevel: '',
energyLevel: '',
goals: [],
timeCommitment: '15',
};
let errors = validate(formData, step);
const handleChangeTest = (field, value) => {
formData = { ...formData, [field]: value };
errors = validate(formData, step);
};
const { rerender } = render(
<ActivityInfo
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
errors={errors}
/>
);
const activitySelect = screen.getByLabelText(
/How often do you exercise\?/i
);
const energySelect = screen.getByLabelText(
/How would you describe your average energy level\?/i
);
await userEvent.selectOptions(activitySelect, '3+ times a week');
await userEvent.selectOptions(energySelect, 'medium');
rerender(
<ActivityInfo
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
errors={errors}
/>
);
expect(
screen.queryByText(/Activity Level is required/i)
).not.toBeInTheDocument();
expect(
screen.queryByText(/Energy Level is required/i)
).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,173 @@
import { it, expect, describe, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import userEvent from '@testing-library/user-event';
import GoalSetting from '../../src/steps/GoalSetting';
import { validate } from '../../src/utils/validation';
describe('GoalSetting Component', () => {
afterEach(cleanup);
const step = 3;
it('renders all form fields', () => {
render(
<GoalSetting
formData={{ goals: [], timeCommitment: '' }}
handleChange={() => {}}
handleCheckboxChange={() => {}}
errors={{}}
/>
);
// Use getByRole to target the fieldset for "What are your goals?"
expect(
screen.getByRole('group', { name: /What are your goals\?/i })
).toBeInTheDocument();
expect(
screen.getByLabelText(/How much time can you commit each day\?/i)
).toBeInTheDocument();
});
it('displays validation errors for empty fields', () => {
const formData = {
goals: [],
timeCommitment: '',
};
const errors = validate(formData, step);
render(
<GoalSetting
formData={formData}
handleChange={() => {}}
handleCheckboxChange={() => {}}
errors={errors}
/>
);
expect(
screen.getByText(/At least one goal is required/i)
).toBeInTheDocument();
expect(
screen.getByText(/Time Commitment is required/i)
).toBeInTheDocument();
});
it('displays no validation errors for valid data', () => {
const formData = {
goals: ['weightLoss', 'muscleGain'],
timeCommitment: '30',
};
const errors = validate(formData, step);
render(
<GoalSetting
formData={formData}
handleChange={() => {}}
handleCheckboxChange={() => {}}
errors={errors}
/>
);
expect(screen.queryByText(/is required/i)).not.toBeInTheDocument();
});
it('handles checkbox interactions and updates goals dynamically', async () => {
let formData = { goals: [], timeCommitment: '' };
let errors = validate(formData, step);
const handleCheckboxChangeTest = (field, id, isChecked) => {
const updatedGoals = isChecked
? [...formData[field], id]
: formData[field].filter((goal) => goal !== id);
formData = { ...formData, [field]: updatedGoals };
errors = validate(formData, step);
};
const { rerender } = render(
<GoalSetting
formData={formData}
handleChange={() => {}}
handleCheckboxChange={handleCheckboxChangeTest}
errors={errors}
/>
);
const weightLossCheckbox = screen.getByLabelText(/Weight Loss/i);
const muscleGainCheckbox = screen.getByLabelText(/Muscle Gain/i);
await userEvent.click(weightLossCheckbox);
rerender(
<GoalSetting
formData={formData}
handleChange={() => {}}
handleCheckboxChange={handleCheckboxChangeTest}
errors={errors}
/>
);
expect(weightLossCheckbox).toBeChecked();
expect(
screen.queryByText(/At least one goal is required/i)
).not.toBeInTheDocument();
await userEvent.click(muscleGainCheckbox);
rerender(
<GoalSetting
formData={formData}
handleChange={() => {}}
handleCheckboxChange={handleCheckboxChangeTest}
errors={errors}
/>
);
expect(muscleGainCheckbox).toBeChecked();
expect(formData.goals).toContain('weightLoss');
expect(formData.goals).toContain('muscleGain');
});
it('handles select interactions and updates time commitment dynamically', async () => {
let formData = { goals: ['weightLoss'], timeCommitment: '' };
let errors = validate(formData, step);
const handleChangeTest = (field, value) => {
formData = { ...formData, [field]: value };
errors = validate(formData, step);
};
const { rerender } = render(
<GoalSetting
formData={formData}
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
handleCheckboxChange={() => {}}
errors={errors}
/>
);
const timeCommitmentSelect = screen.getByLabelText(
/How much time can you commit each day\?/i
);
await userEvent.selectOptions(timeCommitmentSelect, '45');
rerender(
<GoalSetting
formData={formData}
handleChange={({ target }) =>
handleChangeTest(target.name, target.value)
}
handleCheckboxChange={() => {}}
errors={errors}
/>
);
expect(timeCommitmentSelect).toHaveValue('30');
expect(
screen.queryByText(/Time Commitment is required/i)
).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,58 @@
import { describe, it, expect } from 'vitest';
import {
isEmpty,
isValidEmail,
hasMinLength,
hasTwoWords,
} from '../../src/utils/validation';
describe('Utility Functions', () => {
describe('isEmpty', () => {
it('should return true for empty strings', () => {
expect(isEmpty('')).toBe(true);
expect(isEmpty(' ')).toBe(true);
});
it('should return false for non-empty strings', () => {
expect(isEmpty('John')).toBe(false);
expect(isEmpty(' John Doe ')).toBe(false);
});
});
describe('isValidEmail', () => {
it('should validate correct email formats', () => {
expect(isValidEmail('john@example.com')).toBe(true);
expect(isValidEmail('jane.doe@sub.domain.co')).toBe(true);
});
it('should invalidate incorrect email formats', () => {
expect(isValidEmail('johnexample.com')).toBe(false);
expect(isValidEmail('jane@.com')).toBe(false);
expect(isValidEmail('jane@com')).toBe(false);
});
});
describe('hasMinLength', () => {
it('should return true if value meets minimum length', () => {
expect(hasMinLength('password', 6)).toBe(true);
expect(hasMinLength('123456', 6)).toBe(true);
});
it('should return false if value does not meet minimum length', () => {
expect(hasMinLength('pass', 6)).toBe(false);
expect(hasMinLength('123', 6)).toBe(false);
});
});
describe('hasTwoWords', () => {
it('should return true if fullName has at least two words', () => {
expect(hasTwoWords('John Doe')).toBe(true);
expect(hasTwoWords('Jane Mary Doe')).toBe(true);
});
it('should return false if fullName has less than two words', () => {
expect(hasTwoWords('John')).toBe(false);
expect(hasTwoWords(' Jane ')).toBe(false);
});
});
});

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// #region agent log
fetch('http://127.0.0.1:7399/ingest/04244453-f432-4f3b-ade1-92bbdb574a0e',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'a04de5'},body:JSON.stringify({sessionId:'a04de5',runId:'post-fix',hypothesisId:'H3',location:'vite.config.js',message:'vite config loaded',data:{hasReactPlugin:true},timestamp:Date.now()})}).catch(()=>{});
// #endregion
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
test: {
environment: 'jsdom',
},
plugins: [react()],
});

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,14 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.5",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/react-dom": "^19.2.5",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"eslint": "^10.8.1",
"eslint": "^10.9.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"jsdom": "^30.0.1",

View File

@@ -0,0 +1,36 @@
import { it, expect, describe } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import CategorySection from '../components/CategorySection';
import { fakeCategories } from './__mocks__/categories.mock';
import { MemoryRouter } from 'react-router-dom';
const categories = fakeCategories;
describe('CategorySection', () => {
it('should render the heading right', () => {
render(
<MemoryRouter>
<CategorySection categories={categories} />
</MemoryRouter>,
);
screen.debug();
expect(screen.getByText(/Explore by Category/i)).toBeInTheDocument();
});
it('should have 3 links and 3 pics', () => {
render(
<MemoryRouter>
<CategorySection categories={categories} />
</MemoryRouter>,
);
const imgs = screen.getAllByRole('img');
const links = screen.getAllByRole('link');
expect(imgs).toHaveLength(3); //.length()
expect(links).toHaveLength(3);
});
});

View File

@@ -0,0 +1,32 @@
import { it, expect, describe } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { MemoryRouter } from 'react-router-dom';
import { fakeNavItems } from './__mocks__/navItems.mock';
import Footer from '../components/Footer';
const navItems = fakeNavItems;
describe('Footer', () => {
it('should render the navigation items in the footer', () => {
render(
<MemoryRouter>
<Footer navigation={navItems} />
</MemoryRouter>,
);
screen.debug();
navItems.forEach((item) => {
const spanEl = screen.getByText(item.name);
const linkEl = spanEl.parentElement;
expect(spanEl).toBeInTheDocument();
expect(linkEl).toHaveAttribute('href', item.href);
});
// expect(screen.getByText(navItems[0].name)).toBeInTheDocument();
// expect(screen.getByText(navItems[1].name)).toBeInTheDocument();
// expect(screen.getByText(navItems[2].name)).toBeInTheDocument();
// expect(screen.getByText(navItems[3].name)).toBeInTheDocument();
});
});

View File

@@ -57,4 +57,49 @@ describe('ImageGallery', () => {
'https://cdn.dummyjson.com/products/images/womens-bags/Prada%20Women%20Bag/3.png',
);
});
// Aro
it('should display the second image when the arrow key right is pressed', async () => {
const { images } = renderComponent();
const mainImage = images[images.length - 1];
const user = userEvent.setup();
expect(mainImage).toHaveAttribute('src', product.images[0]);
await user.keyboard('{ArrowRight}');
expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[1]);
});
it('should display the first image wenn pressing arrow key right and than arrow key left', async () => {
const { images } = renderComponent();
const mainImage = images[images.length - 1];
const user = userEvent.setup();
expect(mainImage).toHaveAttribute('src', product.images[0]);
await user.keyboard('{ArrowRight}');
expect(mainImage).toHaveAttribute('src', product.images[1]);
await user.keyboard('{ArrowLeft}');
expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[0]);
});
it('should display last image after pressing arrow key left', async () => {
const { images } = renderComponent();
const mainImage = images[images.length - 1];
const user = userEvent.setup();
expect(mainImage).toHaveAttribute('src', product.images[0]);
await user.keyboard('{ArrowLeft}');
expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute('src', product.images[product.images.length - 1]);
});
});

View File

@@ -0,0 +1,21 @@
export const fakeCategories = [
{
imgSrc: 'https://images.unsplash.com/photo-1530011940847-04cba36df39f?q=80&w=1920',
imgAlt: 'Two female models sitting on a bed. Photo by Billie (@billiebodybrand) on Unsplash.',
href: '/women',
linkText: 'Women Clothing',
},
{
imgSrc: 'https://images.unsplash.com/photo-1595909315417-2edd382a56dc?q=80&w=1920',
imgAlt:
'Jumping rope, smartphone, sports bra, a pair of sneakers, and a water bottle. Photo by erica steeves on Unsplash.',
href: '/accessories',
linkText: 'Accessories',
},
{
imgSrc: 'https://images.unsplash.com/photo-1467779009031-53938b78ca38?q=80&w=1920',
imgAlt: 'Group of men sitting in a van. Photo by Luke Porter on Unsplash.',
href: '/men',
linkText: 'Men Clothing',
},
];

View File

@@ -0,0 +1,18 @@
export const fakeNavItems = [
{
name: 'Home 1',
href: '/',
},
{
name: 'Women 1',
href: '/women1',
},
{
name: 'Men 1',
href: '/men1',
},
{
name: 'Accessories 1',
href: '/accessories1',
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -20,18 +20,18 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.5",
"@testing-library/user-event": "^14.6.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/react-dom": "^19.2.5",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"@vitest/ui": "^4.1.11",
"eslint": "^10.8.1",
"eslint": "^10.9.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"jsdom": "^30.0.1",
"typescript": "^6.0.3",
"typescript": "^7.0.2",
"vite": "^8.2.2",
"vitest": "^4.1.11"
}

View File

@@ -19,7 +19,7 @@ describe('Hero', () => {
it('should render the company name when passed', () => {
render(<Hero companyName="My Company" />);
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
// screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
// Arrange
const heading = screen.getByRole('heading', { level: 1 });
@@ -30,7 +30,7 @@ describe('Hero', () => {
it('should render the company name Vogue Junction as default', () => {
render(<Hero />);
screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
// screen.debug(); // console Ausgabe der gerenderten Komponente (HTML-Elemente)
// Arrange
const heading = screen.getByRole('heading', { level: 1 });

View File

@@ -1,8 +1,11 @@
import { it, describe, expect } from 'vitest';
import { add } from '../utils/helpers';
// Test Suite
describe('add', () => {
// Test Case
it('should return 3 when 2 and 1 are passed', () => {
// AAA
expect(add(2, 1)).toBe(3);
});