diff --git a/src/test/app.test.tsx b/src/test/app.test.tsx
index c036a2e..5f7c474 100644
--- a/src/test/app.test.tsx
+++ b/src/test/app.test.tsx
@@ -25,7 +25,12 @@ vi.mock("../components/Header", () => ({
}));
vi.mock("../pages/Server", () => ({
- default: () =>
server-page
,
+ default: ({ backendError }: { backendError?: Error | null }) => (
+
+
server-page
+ {backendError &&
{backendError.message}
}
+
+ ),
}));
vi.mock("../pages/ServerDetail", () => ({
@@ -69,11 +74,14 @@ function renderApp(route = "/") {
window.history.pushState({}, "", route);
const queryClient = createTestQueryClient();
- return render(
-
-
- ,
- );
+ return {
+ queryClient,
+ ...render(
+
+
+ ,
+ ),
+ };
}
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(""),
@@ -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");
diff --git a/src/test/components/info-components.test.tsx b/src/test/components/info-components.test.tsx
index bbed08e..886870b 100644
--- a/src/test/components/info-components.test.tsx
+++ b/src/test/components/info-components.test.tsx
@@ -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(
);
+
+ expect(container.querySelector(".fi-us")).toBeInTheDocument();
+
+ rerender(
);
+
+ 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);
diff --git a/src/test/pages/Server.test.tsx b/src/test/pages/Server.test.tsx
index fb2a525..f8ca860 100644
--- a/src/test/pages/Server.test.tsx
+++ b/src/test/pages/Server.test.tsx
@@ -91,7 +91,10 @@ function StatusControl() {
function renderServerPage(
websocketValue: Partial
,
- { 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 && }
-
+
,
@@ -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({