opengeos--geolibre
7df9ebf22c
Sync labels / sync (push) Failing after 1m9s
Publish Container Image / Build and publish container image (push) Has been cancelled
Deploy Website / Deploy to GitHub Pages (push) Has been cancelled
Deploy Website / Build website and demo (push) Has been cancelled
Deploy viewer / Deploy viewer proxy to Cloudflare Workers (push) Has been cancelled
Deploy tiles / Deploy planetary tile proxy to Cloudflare Workers (push) Has been cancelled
Deploy collab / Deploy collaboration relay to Cloudflare Workers (push) Has been cancelled
CI / Dependency audit (push) Has been cancelled
CI / E2E smoke (Playwright) (push) Has been cancelled
CI / Validate CITATION.cff (push) Has been cancelled
CI / Build and test (push) Has been cancelled
133 行
4.6 KiB
TypeScript
133 行
4.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { before, beforeEach, describe, it } from "node:test";
|
|
|
|
// diagnostics.ts (pulled in transitively by the desktop boundaries) reads
|
|
// localStorage at import time, so the window stub must be installed before the
|
|
// modules are loaded; hence the dynamic imports below.
|
|
const storage = new Map<string, string>();
|
|
(globalThis as { window?: unknown }).window = {
|
|
localStorage: {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
storage.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
storage.delete(key);
|
|
},
|
|
},
|
|
};
|
|
|
|
type ErrorBoundaryModule =
|
|
typeof import("../packages/ui/src/components/error-boundary");
|
|
type BoundariesModule =
|
|
typeof import("../apps/geolibre-desktop/src/components/common/error-boundaries");
|
|
type DiagnosticsModule =
|
|
typeof import("../apps/geolibre-desktop/src/lib/diagnostics");
|
|
|
|
let ErrorBoundary: ErrorBoundaryModule["ErrorBoundary"];
|
|
let resetKeysChanged: ErrorBoundaryModule["resetKeysChanged"];
|
|
let reportBoundaryError: BoundariesModule["reportBoundaryError"];
|
|
let clearDiagnostics: DiagnosticsModule["clearDiagnostics"];
|
|
let getDiagnosticsSnapshot: DiagnosticsModule["getDiagnosticsSnapshot"];
|
|
|
|
before(async () => {
|
|
({ ErrorBoundary, resetKeysChanged } = await import(
|
|
"../packages/ui/src/components/error-boundary"
|
|
));
|
|
({ reportBoundaryError } = await import(
|
|
"../apps/geolibre-desktop/src/components/common/error-boundaries"
|
|
));
|
|
({ clearDiagnostics, getDiagnosticsSnapshot } = await import(
|
|
"../apps/geolibre-desktop/src/lib/diagnostics"
|
|
));
|
|
});
|
|
|
|
describe("ErrorBoundary derived state", () => {
|
|
it("captures the thrown error into state", () => {
|
|
const error = new Error("boom");
|
|
assert.deepEqual(ErrorBoundary.getDerivedStateFromError(error), { error });
|
|
});
|
|
});
|
|
|
|
describe("resetKeysChanged", () => {
|
|
it("treats absent and empty key lists the same (no reset)", () => {
|
|
assert.equal(resetKeysChanged(undefined, undefined), false);
|
|
assert.equal(resetKeysChanged(undefined, []), false);
|
|
assert.equal(resetKeysChanged([], undefined), false);
|
|
assert.equal(resetKeysChanged([], []), false);
|
|
});
|
|
|
|
it("does not reset when keys are unchanged", () => {
|
|
assert.equal(resetKeysChanged(["a", 1], ["a", 1]), false);
|
|
});
|
|
|
|
it("resets when a key value changes", () => {
|
|
assert.equal(resetKeysChanged(["a"], ["b"]), true);
|
|
});
|
|
|
|
it("resets when the key count changes", () => {
|
|
assert.equal(resetKeysChanged(["a"], ["a", "b"]), true);
|
|
assert.equal(resetKeysChanged([], ["a"]), true);
|
|
});
|
|
});
|
|
|
|
describe("ErrorBoundary recovery wiring (componentDidUpdate)", () => {
|
|
// reset() uses setState, which is a no-op on an instance that React has not
|
|
// mounted, so spy on reset to assert the control flow instead.
|
|
function makeBoundary(resetKeys: readonly unknown[] | undefined) {
|
|
const props = {
|
|
children: null,
|
|
fallback: () => null,
|
|
resetKeys,
|
|
};
|
|
const boundary = new ErrorBoundary(props);
|
|
let resetCalls = 0;
|
|
boundary.reset = () => {
|
|
resetCalls += 1;
|
|
};
|
|
return { boundary, props, resetCalls: () => resetCalls };
|
|
}
|
|
|
|
it("resets when a resetKey changes while in the error state", () => {
|
|
const { boundary, props, resetCalls } = makeBoundary(["a"]);
|
|
boundary.state = { error: new Error("boom") };
|
|
boundary.componentDidUpdate({ ...props, resetKeys: ["b"] });
|
|
assert.equal(resetCalls(), 1);
|
|
});
|
|
|
|
it("does not reset when resetKeys are unchanged", () => {
|
|
const { boundary, props, resetCalls } = makeBoundary(["a"]);
|
|
boundary.state = { error: new Error("boom") };
|
|
boundary.componentDidUpdate(props);
|
|
assert.equal(resetCalls(), 0);
|
|
});
|
|
|
|
it("does not reset when there is no active error", () => {
|
|
const { boundary, props, resetCalls } = makeBoundary(["a"]);
|
|
// state.error stays null
|
|
boundary.componentDidUpdate({ ...props, resetKeys: ["b"] });
|
|
assert.equal(resetCalls(), 0);
|
|
});
|
|
});
|
|
|
|
describe("reportBoundaryError", () => {
|
|
beforeEach(() => {
|
|
clearDiagnostics();
|
|
});
|
|
|
|
it("records a runtime error diagnostic the user can see", () => {
|
|
reportBoundaryError("Layer panel", new Error("render failed"), {
|
|
componentStack: "\n at LayerPanel",
|
|
});
|
|
const snapshot = getDiagnosticsSnapshot();
|
|
assert.equal(snapshot.totalCount, 1);
|
|
assert.equal(snapshot.errorCount, 1);
|
|
const [record] = snapshot.records;
|
|
assert.equal(record.category, "runtime");
|
|
assert.equal(record.level, "error");
|
|
assert.match(record.message, /Layer panel crashed: render failed/);
|
|
assert.equal(record.source, "Layer panel");
|
|
assert.ok(record.detail?.includes("at LayerPanel"));
|
|
});
|
|
});
|