Added Error Boundary (#649)

* Added Error Boundary

* Test improvements

* Introduced useDevLogger

* Updated Cypress to latest and aligned the tests

* Added eslint-enable

* Set allowCypressEnv: false for security reasons.
This commit is contained in:
yuri-sakharov
2026-02-08 20:22:04 +02:00
committed by GitHub
parent ea7f8722ac
commit f647a3db03
16 changed files with 763 additions and 536 deletions

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "cypress";
export default defineConfig({
allowCypressEnv: false,
component: {
devServer: {
framework: "react",
@@ -9,8 +10,9 @@ export default defineConfig({
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "http://localhost:5173",
// setupNodeEvents(on, config) {
// // implement node event listeners here
// },
},
});

View File

@@ -4,17 +4,15 @@ describe("Adding repository flow", () => {
const addChartRepositoryButton = "[data-cy='add-chart-repository-button']";
it("Adding new chart repository", () => {
cy.intercept("GET", "http://localhost:5173/status", {
cy.intercept("GET", "/status", {
fixture: "status.json",
}).as("status");
cy.intercept("GET", "http://localhost:5173/api/helm/releases", {
cy.intercept("GET", "/api/helm/releases", {
fixture: "releases.json",
}).as("releases");
cy.visit(
"http://localhost:5173/#/minikube/installed?filteredNamespace=default"
);
cy.visit("/#/minikube/installed?filteredNamespace=default");
cy.get("[data-cy='navigation-link']").contains("Repository").click();
cy.get("[data-cy='install-repository-button']").click();
@@ -22,11 +20,12 @@ describe("Adding repository flow", () => {
cy.get(addChartNameInput).type("Komodorio");
cy.get(addChartUrlInput).type("https://helm-charts.komodor.io");
cy.intercept("GET", "http://localhost:5173/api/helm/repositories", {
cy.intercept("GET", "/api/helm/repositories", {
fixture: "repositories.json",
}).as("repositories");
cy.get(addChartRepositoryButton).click();
cy.wait("@repositories");
cy.contains("https://helm-charts.komodor.io");
@@ -36,15 +35,13 @@ describe("Adding repository flow", () => {
.contains("Install")
.click();
cy.intercept("POST", "http://localhost:5173/api/helm/releases/default", {
cy.intercept("POST", "/api/helm/releases/default", {
fixture: "defaultReleases.json",
}).as("defaultReleases");
cy.intercept(
"GET",
"http://localhost:5173/api/helm/releases/default/helm-dashboard/history",
{ fixture: "history.json" }
).as("history");
cy.intercept("GET", "/api/helm/releases/default/helm-dashboard/history", {
fixture: "history.json",
}).as("history");
cy.contains("Confirm").click();

View File

@@ -1,6 +1,7 @@
import "./commands";
import { mount } from "cypress/react18";
import { mount } from "cypress/react";
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
@@ -8,5 +9,5 @@ declare global {
}
}
}
Cypress.Commands.add("mount", mount);
/* eslint-enable @typescript-eslint/no-namespace */
Cypress.Commands.add("mount", mount);

View File

@@ -18,6 +18,7 @@
"luxon": "^3.7.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.1.0",
"react-icons": "^5.5.0",
"react-intersection-observer": "^10.0.0",
"react-modern-drawer": "^1.4.0",
@@ -43,7 +44,7 @@
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
"babel-plugin-react-compiler": "^1.0.0",
"cypress": "^13.3.0",
"cypress": "15.10.0",
"eslint": "^9.39.1",
"eslint-config-enpitech": "^1.0.17",
"eslint-config-prettier": "^10.1.8",
@@ -409,17 +410,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@cypress/request": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz",
@@ -2926,6 +2916,8 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -2948,6 +2940,8 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -2958,6 +2952,8 @@
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"inBundle": true,
"license": "0BSD",
@@ -3219,6 +3215,7 @@
"integrity": "sha512-22MG6T02Hos2JWfa1o5jsIByn+bc5iOt1IS4xyg6OG68Bu+wMonVZzdrgCw693++rpLE9RUT/Bx15BeDzO0j+g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3310,6 +3307,13 @@
"@types/react": "*"
}
},
"node_modules/@types/tmp": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
"integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -4035,13 +4039,6 @@
"node": ">=8"
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"dev": true,
"license": "MIT"
},
"node_modules/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -4236,9 +4233,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.31",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4566,16 +4563,6 @@
"node": ">= 16"
}
},
"node_modules/check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -4592,9 +4579,9 @@
}
},
"node_modules/ci-info": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
"integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
"dev": true,
"funding": [
{
@@ -4637,9 +4624,9 @@
}
},
"node_modules/cli-table3": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz",
"integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz",
"integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4649,7 +4636,7 @@
"node": "10.* || >= 12.*"
},
"optionalDependencies": {
"@colors/colors": "1.5.0"
"colors": "1.4.0"
}
},
"node_modules/cli-truncate": {
@@ -4729,6 +4716,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -4897,27 +4895,27 @@
"license": "MIT"
},
"node_modules/cypress": {
"version": "13.6.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
"integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
"version": "15.10.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-15.10.0.tgz",
"integrity": "sha512-OtUh7OMrfEjKoXydlAD1CfG2BvKxIqgWGY4/RMjrqQ3BKGBo5JFKoYNH+Tpcj4xKxWH4XK0Xri+9y8WkxhYbqQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@cypress/request": "^3.0.0",
"@cypress/request": "^3.0.10",
"@cypress/xvfb": "^1.2.4",
"@types/node": "^18.17.5",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
"@types/tmp": "^0.2.3",
"arch": "^2.2.0",
"blob-util": "^2.0.2",
"bluebird": "^3.7.2",
"buffer": "^5.6.0",
"buffer": "^5.7.1",
"cachedir": "^2.3.0",
"chalk": "^4.1.0",
"check-more-types": "^2.24.0",
"ci-info": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1",
"cli-table3": "0.6.1",
"commander": "^6.2.1",
"common-tags": "^1.8.0",
"dayjs": "^1.10.4",
@@ -4929,12 +4927,10 @@
"extract-zip": "2.0.1",
"figures": "^3.2.0",
"fs-extra": "^9.1.0",
"getos": "^3.2.1",
"is-ci": "^3.0.0",
"hasha": "5.2.2",
"is-installed-globally": "~0.4.0",
"lazy-ass": "^1.6.0",
"listr2": "^3.8.3",
"lodash": "^4.17.21",
"lodash": "^4.17.23",
"log-symbols": "^4.0.0",
"minimist": "^1.2.8",
"ospath": "^1.2.2",
@@ -4942,9 +4938,10 @@
"process": "^0.11.10",
"proxy-from-env": "1.0.0",
"request-progress": "^3.0.0",
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.1",
"systeminformation": "^5.27.14",
"tmp": "~0.2.4",
"tree-kill": "1.2.2",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},
@@ -4952,7 +4949,7 @@
"cypress": "bin/cypress"
},
"engines": {
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
"node": "^20.1.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/damerau-levenshtein": {
@@ -7415,16 +7412,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/getos": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
"integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"async": "^3.2.0"
}
},
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -7660,6 +7647,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasha": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
"integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-stream": "^2.0.0",
"type-fest": "^0.8.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/hasha/node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -8093,19 +8107,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-ci": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ci-info": "^3.2.0"
},
"bin": {
"is-ci": "bin.js"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -8812,16 +8813,6 @@
"language-subtag-registry": "~0.3.2"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "> 0.8"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -10856,6 +10847,15 @@
"react": "^19.2.0"
}
},
"node_modules/react-error-boundary": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.0.tgz",
"integrity": "sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
@@ -12367,6 +12367,33 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/systeminformation": {
"version": "5.30.7",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.7.tgz",
"integrity": "sha512-33B/cftpaWdpvH+Ho9U1b08ss8GQuLxrWHelbJT1yw4M48Taj8W3ezcPuaLoIHZz5V6tVHuQPr5BprEfnBLBMw==",
"dev": true,
"license": "MIT",
"os": [
"darwin",
"linux",
"win32",
"freebsd",
"openbsd",
"netbsd",
"sunos",
"android"
],
"bin": {
"systeminformation": "lib/cli.js"
},
"engines": {
"node": ">=8.0.0"
},
"funding": {
"type": "Buy me a coffee",
"url": "https://www.buymeacoffee.com/systeminfo"
}
},
"node_modules/tabbable": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
@@ -12564,6 +12591,16 @@
"node": ">=16"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tree-sitter": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
@@ -12865,7 +12902,8 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/universalify": {
"version": "2.0.0",

View File

@@ -14,6 +14,7 @@
"luxon": "^3.7.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.1.0",
"react-icons": "^5.5.0",
"react-intersection-observer": "^10.0.0",
"react-modern-drawer": "^1.4.0",
@@ -39,7 +40,7 @@
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
"babel-plugin-react-compiler": "^1.0.0",
"cypress": "^13.3.0",
"cypress": "15.10.0",
"eslint": "^9.39.1",
"eslint-config-enpitech": "^1.0.17",
"eslint-config-prettier": "^10.1.8",
@@ -80,7 +81,9 @@
"prettier": "npx prettier src/ --check",
"prettier:fix": "npm run prettier -- --write",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"cypress:run": "cypress run",
"cypress:component": "cypress run --component",
"cypress:component:open": "cypress open --component"
},
"keywords": [],
"author": "",

View File

@@ -11,6 +11,8 @@ import { ErrorModalContext } from "./context/ErrorModalContext";
import GlobalErrorModal from "./components/modal/GlobalErrorModal";
import { AppContextProvider } from "./context/AppContext";
import apiService from "./API/apiService";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./components/ErrorFallback";
const DocsPage = lazy(() => import("./pages/DocsPage"));
@@ -53,25 +55,27 @@ export default function App() {
<AppContextProvider>
<ErrorModalContext.Provider value={value}>
<QueryClientProvider client={queryClient}>
<HashRouter>
<Routes>
<Route path="docs/*" element={<DocsPage />} />
<Route path="*" element={<PageLayout />}>
<Route path=":context?/*" element={<SyncContext />}>
<Route
path="repository/:selectedRepo?/*"
element={<RepositoryPage />}
/>
<Route path="installed/?" element={<Installed />} />
<Route
path=":namespace/:chart/installed/revision/:revision"
element={<Revision />}
/>
<Route path="*" element={<Installed />} />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<HashRouter>
<Routes>
<Route path="docs/*" element={<DocsPage />} />
<Route path="*" element={<PageLayout />}>
<Route path=":context?/*" element={<SyncContext />}>
<Route
path="repository/:selectedRepo?/*"
element={<RepositoryPage />}
/>
<Route path="installed/?" element={<Installed />} />
<Route
path=":namespace/:chart/installed/revision/:revision"
element={<Revision />}
/>
<Route path="*" element={<Installed />} />
</Route>
</Route>
</Route>
</Routes>
</HashRouter>
</Routes>
</HashRouter>
</ErrorBoundary>
<GlobalErrorModal
isOpen={!!shouldShowErrorModal}
onClose={() => setShowErrorModal(undefined)}

View File

@@ -1,4 +1,4 @@
import { mount } from "cypress/react18";
import { mount } from "cypress/react";
import { Button } from "./common/Button/Button";
describe("Button component tests", () => {

View File

@@ -45,6 +45,14 @@ const renderClustersList = (props: ClustersListProps) => {
describe("ClustersList", () => {
it("Got one cluster information", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({
selectedCluster: "minikube",
filteredNamespaces: ["default"],
@@ -52,12 +60,21 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()],
});
cy.wait("@getClusters");
cy.get(".data-cy-clusterName").contains("minikube");
cy.get(".data-cy-clusterList-namespace").contains("default");
cy.get(".data-cy-clustersInput").should("be.checked");
});
it("Dont have a cluster chekced", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({
selectedCluster: "",
filteredNamespaces: [""],
@@ -65,6 +82,7 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()],
});
cy.wait("@getClusters");
cy.get(".data-cy-clustersInput").should("not.be.checked");
});
});

View File

@@ -0,0 +1,113 @@
import ErrorFallback from "./ErrorFallback";
import { mount } from "cypress/react";
import { ErrorBoundary } from "react-error-boundary";
import { useState } from "react";
/**
* Component tests for ErrorFallback
* Tests the error fallback UI and reset functionality
*/
describe("ErrorFallback", () => {
beforeEach(() => {
// Ensure portal root exists for createPortal
if (!document.getElementById("portal")) {
const portalDiv = document.createElement("div");
portalDiv.id = "portal";
document.body.appendChild(portalDiv);
}
});
it("should render error modal with error message and hint", () => {
const mockError = new Error("Test error message");
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Verify modal is open (checking document directly because of portal)
cy.get("#portal").should("be.visible");
cy.get("#portal").should("contain", "Application Error");
cy.get("#portal").should("contain", "Test error message");
// Verify Komodor hint is present (from GlobalErrorModal)
cy.get("#portal").should("contain", "Sign up for free.");
cy.get("#portal a")
.should("have.attr", "href")
.and("include", "komodor.com");
});
it("should call resetErrorBoundary when modal is closed", () => {
const mockError = new Error("Test error");
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Find and click close button (using the selector from Modal.tsx)
cy.get("[data-modal-hide='staticModal']").click();
// Verify reset was called
cy.get("@resetErrorBoundary").should("have.been.calledOnce");
});
it("should handle non-Error objects gracefully", () => {
const mockError = "String error" as unknown as Error;
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Should show fallback message
cy.get("#portal").should(
"contain",
"An unexpected error occurred. Please try again."
);
});
it("should log error in development mode", () => {
const mockError = new Error("Test error for logging");
const mockReset = cy.stub();
cy.window().then((win) => {
cy.spy(win.console, "error").as("consoleError");
});
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// In dev mode, error should be logged
cy.get("@consoleError").should("have.been.called");
});
it("should catch errors from a real component and recover after reset (Integration)", () => {
const BuggyComponent = ({ shouldCrash }: { shouldCrash: boolean }) => {
if (shouldCrash) {
throw new Error("Integrated crash");
}
return <div data-cy="recovered">Recovered successfully!</div>;
};
const TestWrapper = () => {
const [shouldCrash, setShouldCrash] = useState(true);
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setShouldCrash(false)}
>
<BuggyComponent shouldCrash={shouldCrash} />
</ErrorBoundary>
);
};
mount(<TestWrapper />);
// Verify modal caught the real throw
cy.get("#portal").should("be.visible").and("not.be.empty");
cy.get("#portal").should("contain", "Integrated crash");
// Click close to reset
cy.get("[data-modal-hide='staticModal']").click();
// Verify modal is gone (portal should be empty) and component recovered
cy.get("#portal").should("be.empty");
cy.get("[data-cy='recovered']")
.should("be.visible")
.and("contain", "Recovered successfully!");
});
});

View File

@@ -0,0 +1,116 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import ErrorFallback from "./ErrorFallback";
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import Button from "../Button";
const meta = {
title: "Components/ErrorFallback",
component: ErrorFallback,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
resetErrorBoundary: { action: "reset" },
},
} satisfies Meta<typeof ErrorFallback>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Default error fallback with a standard error message
*/
export const Default: Story = {
args: {
error: new Error("Something went wrong in the application"),
resetErrorBoundary: () => {},
},
};
export const InteractiveIntegration: Story = {
args: {
error: new Error("Interactive Demo Error"),
resetErrorBoundary: () => {},
},
render: (args) => {
const BuggyComponent = () => {
const [shouldError, setShouldError] = useState(false);
if (shouldError) {
throw new Error(
"This is a real runtime error caught by the ErrorBoundary!"
);
}
return (
<div className="w-96 rounded border bg-white p-8 text-center shadow-md">
<h3 className="mb-4 text-lg font-bold">Interactive Demo</h3>
<p className="mb-6 text-sm text-gray-600">
Clicking the button below will cause this component to throw a
runtime error during render.
</p>
<Button onClick={() => setShouldError(true)}>
Trigger Runtime Error
</Button>
</div>
);
};
return (
<ErrorBoundary
FallbackComponent={(props) => (
<ErrorFallback
{...props}
resetErrorBoundary={() => {
props.resetErrorBoundary();
args.resetErrorBoundary();
}}
/>
)}
>
<BuggyComponent />
</ErrorBoundary>
);
},
};
/**
* Long error message to test text wrapping
*/
export const LongErrorMessage: Story = {
args: {
error: new Error(
"This is a very long error message that should demonstrate how the error modal handles text wrapping and displays lengthy error descriptions to the user. The error boundary should gracefully handle this scenario."
),
resetErrorBoundary: () => {},
},
};
/**
* Non-Error object to test fallback behavior
*/
export const NonErrorObject: Story = {
args: {
error: "String error message" as unknown as Error,
resetErrorBoundary: () => {},
},
};
/**
* Error with stack trace (useful for development)
*/
export const WithStackTrace: Story = {
args: {
error: (() => {
try {
throw new Error("Error with detailed stack trace");
} catch (e) {
return e as Error;
}
})(),
resetErrorBoundary: () => {},
},
};

View File

@@ -0,0 +1,33 @@
import type { FallbackProps } from "react-error-boundary";
import GlobalErrorModal from "../modal/GlobalErrorModal";
import { useDevLogger } from "../../hooks/useDevLogger";
/**
* Error fallback component for React Error Boundary
* Uses the existing GlobalErrorModal for consistent error display
* @param error - The error that was caught
* @param resetErrorBoundary - Function to reset the error boundary state
*/
const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
useDevLogger(error);
const handleClose = () => {
// Reset the error boundary to allow the component tree to re-render
resetErrorBoundary();
};
return (
<GlobalErrorModal
isOpen={true}
onClose={handleClose}
titleText="Application Error"
contentText={
error instanceof Error
? error.message
: "An unexpected error occurred. Please try again."
}
/>
);
};
export default ErrorFallback;

View File

@@ -0,0 +1 @@
export { default } from "./ErrorFallback";

View File

@@ -0,0 +1,9 @@
import { useEffect } from "react";
export const useDevLogger = (error: unknown) => {
useEffect(() => {
if (import.meta.env.DEV) {
console.error("UnexpectedError", error);
}
}, [error]);
};

View File

@@ -14,8 +14,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["cypress"]
"jsx": "react-jsx"
},
"include": [
"src",

View File

@@ -7,5 +7,5 @@
"isolatedModules": true,
"strict": true
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "cypress.config.ts"]
}

File diff suppressed because it is too large Load Diff