Files
paseo/packages/app/src/components/root-error-details.test.ts
T
Mohamed BoudraandGitHub cab0e4b3f8 Show a recovery screen for app render errors (#1924)
* fix(app): show recovery screen for render failures

Render a retryable fallback with useful error details when the routed app tree throws during render instead of leaving the app blank.

* test(app): cover root error details without mounting

Move caught-value formatting into a pure module, cover AggregateError fields without duplicate output, and remove the component-mounted test.

* fix(app): keep empty error details caught

* fix(app): preserve nullable aggregate errors

* fix(app): guard malformed error details

* fix(app): handle recursive error causes

* fix(app): guard error detail formatting failures

* fix(app): localize root error fallback

* fix(app): keep root error fallback on existing i18n
2026-07-09 00:06:29 +08:00

110 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { formatCaughtValue } from "./root-error-details";
describe("formatCaughtValue", () => {
it("preserves details for Error values", () => {
class RouteRenderError extends Error {
code = "E_ROUTE_RENDER";
cause = "workspace route";
constructor() {
super("route render exploded");
this.name = "RouteRenderError";
this.stack = "RouteRenderError: route render exploded\n at WorkspaceRoute";
}
}
const details = formatCaughtValue(new RouteRenderError());
expect(details).toContain("Name: RouteRenderError");
expect(details).toContain("Message: route render exploded");
expect(details).toContain("Stack:");
expect(details).toContain("RouteRenderError: route render exploded");
expect(details).toContain("Cause:");
expect(details).toContain("workspace route");
expect(details).toContain("E_ROUTE_RENDER");
});
it("does not duplicate aggregate errors as custom fields", () => {
const error = new AggregateError([new Error("first failure")], "multiple failures");
const details = formatCaughtValue(error);
expect(details).toContain("Errors:");
expect(details).toContain("first failure");
expect(details).not.toContain("Fields:");
});
it("preserves null aggregate error values", () => {
class ErrorWithNullableErrors extends Error {
errors = null;
}
const details = formatCaughtValue(new ErrorWithNullableErrors("nullable errors"));
expect(details).toContain("Errors:\nnull");
expect(details).not.toContain("Fields:");
});
it("does not throw for malformed Error text fields", () => {
const error = new Error("fallback");
Object.defineProperties(error, {
name: { configurable: true, value: null },
message: { configurable: true, value: 42 },
stack: { configurable: true, value: { frame: "bad stack" } },
});
const details = formatCaughtValue(error);
expect(details).toContain("Name: null");
expect(details).toContain("Message: 42");
expect(details).toContain('"frame": "bad stack"');
});
it("marks recursive Error causes", () => {
const error = new Error("self cause");
Object.defineProperty(error, "cause", { configurable: true, value: error });
const details = formatCaughtValue(error);
expect(details).toContain("Cause:\n[Circular Error]");
});
it("returns fallback details when Error properties throw", () => {
const error = new Error("fallback");
Object.defineProperty(error, "message", {
configurable: true,
get() {
throw new Error("bad message getter");
},
});
const details = formatCaughtValue(error);
expect(details).toContain("[Unserializable value]");
expect(details).toContain("Details unavailable:");
expect(details).toContain("Error: bad message getter");
});
it("renders string thrown values as the string", () => {
expect(formatCaughtValue("plain failure")).toBe("plain failure");
});
it("preserves empty string thrown values", () => {
expect(formatCaughtValue("")).toBe("");
});
it("renders numeric thrown values without extra category text", () => {
const details = formatCaughtValue(42);
expect(details).toBe("42");
expect(details).not.toContain("non-Error");
});
it("renders circular objects as JSON with circular markers", () => {
const value: { label: string; self?: unknown } = { label: "loop" };
value.self = value;
expect(formatCaughtValue(value)).toBe('{\n "label": "loop",\n "self": "[Circular]"\n}');
});
});