diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/package-lock.json b/05_react/unterricht/tag51/01_react-app-with-tests/package-lock.json
index 4fdf385..28abe72 100644
--- a/05_react/unterricht/tag51/01_react-app-with-tests/package-lock.json
+++ b/05_react/unterricht/tag51/01_react-app-with-tests/package-lock.json
@@ -26,7 +26,7 @@
"axios-mock-adapter": "^2.1.0",
"cypress": "^15.21.0",
"jsdom": "^30.0.1",
- "oxlint": "^1.79.0",
+ "oxlint": "^1.80.0",
"sass-embedded": "^1.103.1",
"vite": "^8.2.2",
"vitest": "^4.1.11"
@@ -388,9 +388,9 @@
}
},
"node_modules/@oxlint/binding-android-arm-eabi": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz",
+ "integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==",
"cpu": [
"arm"
],
@@ -405,9 +405,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
- "version": "1.79.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz",
- "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz",
+ "integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==",
"cpu": [
"arm64"
],
@@ -422,9 +422,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz",
+ "integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==",
"cpu": [
"arm64"
],
@@ -439,9 +439,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz",
+ "integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==",
"cpu": [
"x64"
],
@@ -456,9 +456,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz",
+ "integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==",
"cpu": [
"x64"
],
@@ -473,9 +473,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz",
+ "integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==",
"cpu": [
"arm"
],
@@ -490,9 +490,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz",
+ "integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==",
"cpu": [
"arm"
],
@@ -507,9 +507,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz",
+ "integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==",
"cpu": [
"arm64"
],
@@ -527,9 +527,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz",
+ "integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==",
"cpu": [
"arm64"
],
@@ -547,9 +547,9 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz",
+ "integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==",
"cpu": [
"ppc64"
],
@@ -567,9 +567,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz",
+ "integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==",
"cpu": [
"riscv64"
],
@@ -587,9 +587,9 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz",
+ "integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==",
"cpu": [
"riscv64"
],
@@ -607,9 +607,9 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz",
+ "integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==",
"cpu": [
"s390x"
],
@@ -627,9 +627,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz",
+ "integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==",
"cpu": [
"x64"
],
@@ -647,9 +647,9 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz",
+ "integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==",
"cpu": [
"x64"
],
@@ -667,9 +667,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz",
+ "integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==",
"cpu": [
"arm64"
],
@@ -684,9 +684,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz",
+ "integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==",
"cpu": [
"arm64"
],
@@ -701,9 +701,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz",
+ "integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==",
"cpu": [
"ia32"
],
@@ -718,9 +718,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
- "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==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz",
+ "integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==",
"cpu": [
"x64"
],
@@ -3931,9 +3931,9 @@
"license": "MIT"
},
"node_modules/oxlint": {
- "version": "1.79.0",
- "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz",
- "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==",
+ "version": "1.80.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz",
+ "integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3946,25 +3946,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
- "@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"
+ "@oxlint/binding-android-arm-eabi": "1.80.0",
+ "@oxlint/binding-android-arm64": "1.80.0",
+ "@oxlint/binding-darwin-arm64": "1.80.0",
+ "@oxlint/binding-darwin-x64": "1.80.0",
+ "@oxlint/binding-freebsd-x64": "1.80.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.80.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.80.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.80.0",
+ "@oxlint/binding-linux-arm64-musl": "1.80.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.80.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.80.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.80.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.80.0",
+ "@oxlint/binding-linux-x64-gnu": "1.80.0",
+ "@oxlint/binding-linux-x64-musl": "1.80.0",
+ "@oxlint/binding-openharmony-arm64": "1.80.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.80.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.80.0",
+ "@oxlint/binding-win32-x64-msvc": "1.80.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=7.0.2001",
diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/package.json b/05_react/unterricht/tag51/01_react-app-with-tests/package.json
index c930593..218d494 100644
--- a/05_react/unterricht/tag51/01_react-app-with-tests/package.json
+++ b/05_react/unterricht/tag51/01_react-app-with-tests/package.json
@@ -31,7 +31,7 @@
"axios-mock-adapter": "^2.1.0",
"cypress": "^15.21.0",
"jsdom": "^30.0.1",
- "oxlint": "^1.79.0",
+ "oxlint": "^1.80.0",
"sass-embedded": "^1.103.1",
"vite": "^8.2.2",
"vitest": "^4.1.11"
diff --git a/05_react/unterricht/tag51/01_react-app-with-tests/src/components/examples/ExampleUserList.jsx b/05_react/unterricht/tag51/01_react-app-with-tests/src/components/examples/ExampleUserList.jsx
index f83344d..4565619 100644
--- a/05_react/unterricht/tag51/01_react-app-with-tests/src/components/examples/ExampleUserList.jsx
+++ b/05_react/unterricht/tag51/01_react-app-with-tests/src/components/examples/ExampleUserList.jsx
@@ -4,9 +4,7 @@ import { useEffect, useState } from 'react';
const ExampleUserList = (props) => {
const [users, setUsers] = useState([]);
useEffect(() => {
- axios
- .get('https://dummyjson.com/users?limit=3')
- .then((response) => setUsers(response.data.users));
+ axios.get('https://dummyjson.com/users?limit=3').then((response) => setUsers(response.data.users));
}, []);
return (
+ );
+};
+export default FormContact;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/components/inputs/InputAmount.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/components/inputs/InputAmount.jsx
new file mode 100644
index 0000000..b78c8af
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/components/inputs/InputAmount.jsx
@@ -0,0 +1,54 @@
+import { useState } from 'react';
+import { FaMinus, FaPlus } from 'react-icons/fa6';
+const InputAmount = () => {
+ const [amount, setAmount] = useState(0);
+
+ const handleChange = (e) => {
+ const value = e.target.value;
+ if (!isNaN(value)) {
+ setAmount(Number(value));
+ }
+ };
+
+ const handleClickIncrement = () => {
+ setAmount(amount + 1);
+ };
+
+ const handleClickDecrement = () => {
+ setAmount(amount - 1);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default InputAmount;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListTodo.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListTodo.jsx
new file mode 100644
index 0000000..07b526c
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListTodo.jsx
@@ -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 && (
+
+ )}
+ {error && {error?.message}
}
+ >
+ );
+};
+export default ListTodo;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListUser.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListUser.jsx
new file mode 100644
index 0000000..442d44f
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/components/lists/ListUser.jsx
@@ -0,0 +1,80 @@
+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 ?? []);
+ setError(null);
+ })
+ .catch((err) => {
+ //
+ // if (err.name === 'AbortError') {}
+ if (axios.isCancel(err)) {
+ console.error('Request abgebrochen:', err.message);
+ } else {
+ console.error('Error:', err);
+ setError(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 is loading users...
;
+ }
+
+ if (error) {
+ const status = error.response?.status;
+ return (
+
+ {status ? `Failed to load users (HTTP ${status})` : 'Failed to load users'}
+
+ );
+ }
+
+ return (
+
+ );
+};
+export default ListUser;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/main.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/main.jsx
new file mode 100644
index 0000000..4233564
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/main.jsx
@@ -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(
+
+
+ ,
+);
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/Button.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/Button.test.jsx
new file mode 100644
index 0000000..485aeaa
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/Button.test.jsx
@@ -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();
+
+ const user = userEvent.setup();
+
+ const button = screen.getByRole('button');
+
+ // fireEvent.click(button);
+ await user.click(button);
+
+ expect(button).toHaveTextContent(/Clicked/i);
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleAsyncComponent.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleAsyncComponent.test.jsx
new file mode 100644
index 0000000..42dff64
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleAsyncComponent.test.jsx
@@ -0,0 +1,27 @@
+// ExampleAsyncComponent.test.jsx
+import { it, describe } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import ExampleAsyncComponent from '../components/examples/ExampleAsyncComponent'; // Adjust the path
+
+// example async component
+// const ExampleAsyncComponent = () => {
+// const [data, setData] = useState('');
+// useEffect(() => {
+// setTimeout(() => {
+// setData('Data loaded');
+// }, 500);
+// }, []);
+// return {data}
;
+// };
+
+describe('ExampleAsyncComponent', () => {
+ it('should render data after loading', async () => {
+ // async function!
+ render();
+
+ // waitFor waits for the callback to not throw an error
+ await waitFor(() => {
+ expect(screen.getByText('Data loaded')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleComponent.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleComponent.test.jsx
new file mode 100644
index 0000000..c9315d8
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleComponent.test.jsx
@@ -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();
+
+ const element = screen.getByText(/Hello World/i);
+
+ expect(element).toBeInTheDocument();
+});
+
+// console.log(document.querySelector('p').textContent.includes('Hello World')); // => true
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleUserList.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleUserList.test.jsx
new file mode 100644
index 0000000..47208db
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/ExampleUserList.test.jsx
@@ -0,0 +1,31 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, afterEach } from 'vitest';
+
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import ExampleUserList from '../components/examples/ExampleUserList';
+
+// Create a new instance of the MockAdapter
+const mock = new MockAdapter(axios);
+
+// Clean up after each test case to prevent interference between tests
+afterEach(() => {
+ mock.reset();
+});
+
+describe('ExampleUserList', () => {
+ it('should load and display user data', async () => {
+ const mockData = { users: [{ id: 1, firstName: 'John' }] };
+ mock.onGet('https://dummyjson.com/users?limit=3').reply(200, mockData);
+
+ render();
+
+ // Configure the mock: When this specific URL is requested, return 200 and our mockData
+ // This intercepts the network call, so no real internet request happens
+ const userElement = await screen.findByText(/John/i);
+
+ expect(userElement).toBeInTheDocument();
+ // expect(await screen.findByText(/John/i)).toBeInTheDocument();
+ // screen.debug();
+ });
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/FormContact.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/FormContact.test.jsx
new file mode 100644
index 0000000..116ebd4
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/FormContact.test.jsx
@@ -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();
+
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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');
+ });
+ });
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/InputAmount.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/InputAmount.test.jsx
new file mode 100644
index 0000000..4ba0280
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/InputAmount.test.jsx
@@ -0,0 +1,14 @@
+// InputAmount.test.jsx
+import { render, screen } from '@testing-library/react';
+import { it, expect, describe } from 'vitest';
+import InputAmount from '../components/inputs/InputAmount'; // Adjust the path
+
+describe('InputAmount', () => {
+ it('should matches the snapshot', () => {
+ const { container } = render();
+
+ screen.debug();
+
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/ListUser.test.jsx b/05_react/unterricht/tag52/01_react-app-performance/src/test/ListUser.test.jsx
new file mode 100644
index 0000000..9b9fd80
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/ListUser.test.jsx
@@ -0,0 +1,91 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, beforeEach, afterEach, vi } from 'vitest';
+
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import ListUser from '../components/lists/ListUser';
+
+const USERS_URL = 'https://dummyjson.com/users?limit=100';
+
+const mockUsersResponse = {
+ users: [
+ {
+ id: 1,
+ firstName: 'John',
+ lastName: 'Doe',
+ age: 28,
+ email: 'john.doe@example.com',
+ },
+ ],
+ total: 1,
+ skip: 0,
+ limit: 100,
+};
+
+const mock = new MockAdapter(axios);
+
+beforeEach(() => {
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ vi.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+afterEach(() => {
+ mock.reset();
+ vi.restoreAllMocks();
+});
+
+describe('ListUser', () => {
+ it('should load and display user data on HTTP 200', async () => {
+ mock.onGet(USERS_URL).reply(200, mockUsersResponse);
+
+ render();
+
+ expect(screen.getByText(/is loading users/i)).toBeInTheDocument();
+
+ expect(await screen.findByText(/John/i)).toBeInTheDocument();
+ expect(screen.getByText(/Doe/i)).toBeInTheDocument();
+ expect(screen.getByText(/28/)).toBeInTheDocument();
+ expect(screen.getByText(/john.doe@example.com/i)).toBeInTheDocument();
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ it('should show an empty list on HTTP 200 when no users are returned', async () => {
+ mock.onGet(USERS_URL).reply(200, { users: [], total: 0, skip: 0, limit: 100 });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it.each([
+ [400, 'Bad Request'],
+ [401, 'Unauthorized'],
+ [403, 'Forbidden'],
+ [404, 'Not Found'],
+ [500, 'Internal Server Error'],
+ [503, 'Service Unavailable'],
+ ])('should show an error for HTTP %s (%s)', async (status) => {
+ mock.onGet(USERS_URL).reply(status);
+
+ render();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent(`Failed to load users (HTTP ${status})`);
+ expect(screen.queryByText(/John/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/is loading users/i)).not.toBeInTheDocument();
+ });
+
+ it('should show a generic error on network failure', async () => {
+ mock.onGet(USERS_URL).networkError();
+
+ render();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent('Failed to load users');
+ expect(screen.queryByText(/HTTP/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/John/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/__snapshots__/InputAmount.test.jsx.snap b/05_react/unterricht/tag52/01_react-app-performance/src/test/__snapshots__/InputAmount.test.jsx.snap
new file mode 100644
index 0000000..f528065
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/__snapshots__/InputAmount.test.jsx.snap
@@ -0,0 +1,65 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`InputAmount > should matches the snapshot 1`] = `
+
+`;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/test/setup.js b/05_react/unterricht/tag52/01_react-app-performance/src/test/setup.js
new file mode 100644
index 0000000..67c151f
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/test/setup.js
@@ -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
diff --git a/05_react/unterricht/tag52/01_react-app-performance/src/utils/validators/validate.js b/05_react/unterricht/tag52/01_react-app-performance/src/utils/validators/validate.js
new file mode 100644
index 0000000..a186292
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/src/utils/validators/validate.js
@@ -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;
diff --git a/05_react/unterricht/tag52/01_react-app-performance/vite.config.js b/05_react/unterricht/tag52/01_react-app-performance/vite.config.js
new file mode 100644
index 0000000..b4a17ce
--- /dev/null
+++ b/05_react/unterricht/tag52/01_react-app-performance/vite.config.js
@@ -0,0 +1,16 @@
+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
+ // testTimeout: 2000, // 10 Sekunden für alle Tests
+ // hookTimeout: 2000, // Optional: Timeout für beforeAll, afterAll, etc.
+ },
+});