fix webui refresh location routing

This commit is contained in:
chengyongru
2026-06-02 14:08:47 +08:00
committed by Xubin Ren
parent b2cabb2bd8
commit 1886d22352
3 changed files with 292 additions and 34 deletions
+84
View File
@@ -199,6 +199,7 @@ describe("App layout", () => {
toggleThemeSpy.mockReset();
attachSpy.mockReset();
runStatusHandlers.clear();
window.history.replaceState(null, "", "/");
localStorage.removeItem("nanobot-webui.sidebar.completed-runs.v1");
vi.mocked(fetchBootstrap).mockReset().mockResolvedValue({
token: "tok",
@@ -628,6 +629,44 @@ describe("App layout", () => {
expect(attachSpy).toHaveBeenCalledWith("chat-a");
});
it("restores the active chat from the URL hash after a page reload", async () => {
mockSessions = [
{
key: "websocket:chat-a",
channel: "websocket",
chatId: "chat-a",
createdAt: "2026-04-16T10:00:00Z",
updatedAt: "2026-04-16T10:00:00Z",
preview: "Active after reload",
},
{
key: "websocket:chat-b",
channel: "websocket",
chatId: "chat-b",
createdAt: "2026-04-16T11:00:00Z",
updatedAt: "2026-04-16T11:00:00Z",
preview: "Other chat",
},
];
window.history.replaceState(
null,
"",
`/#/chat/${encodeURIComponent("websocket:chat-a")}`,
);
render(<App />);
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
await waitFor(() => expect(document.title).toBe("Active after reload · nanobot"));
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
expect(
within(sidebar).getByRole("button", { name: /^Active after reload$/ }),
).toBeInTheDocument();
expect(window.location.hash).toBe(
`#/chat/${encodeURIComponent("websocket:chat-a")}`,
);
});
it("opens the settings view from the sidebar footer", async () => {
mockSessions = [
{
@@ -991,6 +1030,51 @@ describe("App layout", () => {
expect(screen.getByRole("button", { name: "Save" })).toBeEnabled();
});
it("restores the settings section from the URL hash after a page reload", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
if (String(input) === "/api/settings") {
return jsonResponse(baseSettingsPayload());
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
}),
);
window.history.replaceState(null, "", "/#/settings?section=models");
render(<App />);
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
expect(await screen.findByRole("heading", { name: "Models" })).toBeInTheDocument();
expect(window.location.hash).toBe("#/settings?section=models");
});
it("updates the URL hash when switching settings sections", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
if (String(input) === "/api/settings") {
return jsonResponse(baseSettingsPayload());
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
}),
);
render(<App />);
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
fireEvent.click(within(sidebar).getByRole("button", { name: "Settings" }));
expect(await screen.findByRole("heading", { name: "Overview" })).toBeInTheDocument();
expect(window.location.hash).toBe("#/settings");
const settingsNav = screen.getByRole("navigation", { name: "Settings sections" });
fireEvent.click(within(settingsNav).getByRole("button", { name: "Models" }));
expect(await screen.findByRole("heading", { name: "Models" })).toBeInTheDocument();
expect(window.location.hash).toBe("#/settings?section=models");
});
it("opens Apps from the main sidebar without replacing the sidebar", async () => {
vi.stubGlobal(
"fetch",