mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-21 02:30:15 +00:00
feat: implement backend error handling and improve error messaging in Server component
This commit is contained in:
+56
-7
@@ -25,7 +25,12 @@ vi.mock("../components/Header", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../pages/Server", () => ({
|
||||
default: () => <div>server-page</div>,
|
||||
default: ({ backendError }: { backendError?: Error | null }) => (
|
||||
<div>
|
||||
<div>server-page</div>
|
||||
{backendError && <p>{backendError.message}</p>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../pages/ServerDetail", () => ({
|
||||
@@ -69,11 +74,14 @@ function renderApp(route = "/") {
|
||||
window.history.pushState({}, "", route);
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return {
|
||||
queryClient,
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe("App", () => {
|
||||
@@ -110,6 +118,20 @@ describe("App", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the app shell while initial settings are still pending", async () => {
|
||||
appMocks.fetchSetting.mockImplementation(
|
||||
() => new Promise(() => undefined),
|
||||
);
|
||||
|
||||
renderApp();
|
||||
|
||||
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||
expect(screen.getByText("refresh-toast")).toBeInTheDocument();
|
||||
expect(screen.getByText("header")).toBeInTheDocument();
|
||||
expect(screen.getByText("dash-command")).toBeInTheDocument();
|
||||
expect(screen.getByText("footer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("injects custom code before showing the app shell", async () => {
|
||||
appMocks.fetchSetting.mockResolvedValue(
|
||||
settingResponse("<script>custom</script>"),
|
||||
@@ -125,14 +147,41 @@ describe("App", () => {
|
||||
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders fetch errors through the error page", async () => {
|
||||
it("renders the app shell when initial settings fetch fails", async () => {
|
||||
appMocks.fetchSetting.mockRejectedValue(new Error("settings failed"));
|
||||
|
||||
renderApp();
|
||||
|
||||
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||
expect(screen.getByText("refresh-toast")).toBeInTheDocument();
|
||||
expect(screen.getByText("header")).toBeInTheDocument();
|
||||
expect(screen.getByText("dash-command")).toBeInTheDocument();
|
||||
expect(screen.getByText("footer")).toBeInTheDocument();
|
||||
expect(await screen.findByText("settings failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps rendering with stale settings when a later settings refetch fails", async () => {
|
||||
let requestCount = 0;
|
||||
appMocks.fetchSetting.mockImplementation(() => {
|
||||
requestCount += 1;
|
||||
return requestCount === 1
|
||||
? Promise.resolve(settingResponse())
|
||||
: Promise.reject(new Error("settings failed"));
|
||||
});
|
||||
|
||||
const { queryClient } = renderApp();
|
||||
|
||||
expect(await screen.findByText("server-page")).toBeInTheDocument();
|
||||
|
||||
await queryClient.refetchQueries({ queryKey: ["setting"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(appMocks.fetchSetting.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
expect(screen.getByText("server-page")).toBeInTheDocument();
|
||||
expect(screen.queryByText("settings failed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("routes server detail paths through the app router", async () => {
|
||||
renderApp("/server/42");
|
||||
|
||||
|
||||
@@ -180,6 +180,17 @@ describe("ServerFlag", () => {
|
||||
expect(container.querySelector(".fi-us")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("normalizes SVG flag classes and ignores invalid country codes", () => {
|
||||
Object.assign(window, { ForceUseSvgFlag: true });
|
||||
const { container, rerender } = render(<ServerFlag country_code="US" />);
|
||||
|
||||
expect(container.querySelector(".fi-us")).toBeInTheDocument();
|
||||
|
||||
rerender(<ServerFlag country_code="u" />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("uses emoji flags when the canvas probe detects support", async () => {
|
||||
Object.assign(window, { ForceUseSvgFlag: false });
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
|
||||
@@ -91,7 +91,10 @@ function StatusControl() {
|
||||
|
||||
function renderServerPage(
|
||||
websocketValue: Partial<WebSocketContextType>,
|
||||
{ withStatusControl = false } = {},
|
||||
{
|
||||
backendError = null,
|
||||
withStatusControl = false,
|
||||
}: { backendError?: Error | null; withStatusControl?: boolean } = {},
|
||||
) {
|
||||
const defaultWebsocketValue: WebSocketContextType = {
|
||||
lastData: null,
|
||||
@@ -109,7 +112,7 @@ function renderServerPage(
|
||||
value={{ ...defaultWebsocketValue, ...websocketValue }}
|
||||
>
|
||||
{withStatusControl && <StatusControl />}
|
||||
<Servers />
|
||||
<Servers backendError={backendError} />
|
||||
</WebSocketContext.Provider>
|
||||
</StatusProvider>
|
||||
</SortProvider>,
|
||||
@@ -178,6 +181,42 @@ describe("Servers page", () => {
|
||||
expect(screen.getByText("info.processing")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a centered backend error instead of a 500 page", async () => {
|
||||
renderServerPage(
|
||||
{
|
||||
connected: false,
|
||||
lastData: null,
|
||||
},
|
||||
{ backendError: new Error("settings failed") },
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("error.backendUnavailableTitle"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("error.backendUnavailableDescription"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("settings failed")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("info.websocketConnecting"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows backend query errors while waiting for websocket data", async () => {
|
||||
apiMocks.fetchServerGroup.mockRejectedValue(new Error("group failed"));
|
||||
apiMocks.fetchService.mockRejectedValue(new Error("service failed"));
|
||||
|
||||
renderServerPage({
|
||||
connected: false,
|
||||
lastData: null,
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText("error.backendUnavailableTitle"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("group failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("summarizes online and offline servers from websocket data", async () => {
|
||||
const online = createServer({ id: 1, name: "alpha" });
|
||||
const offline = createServer({
|
||||
|
||||
Reference in New Issue
Block a user