From 4f436922ca763d86710c9e79a78f0649d693b518 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Wed, 13 Aug 2025 17:07:59 -0400 Subject: [PATCH] fix: browser title not updating when conversation title changes (#10275) Co-authored-by: openhands --- .../use-document-title-from-state.test.tsx | 135 ++++++++++++++++++ .../hooks/use-document-title-from-state.ts | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 frontend/__tests__/hooks/use-document-title-from-state.test.tsx diff --git a/frontend/__tests__/hooks/use-document-title-from-state.test.tsx b/frontend/__tests__/hooks/use-document-title-from-state.test.tsx new file mode 100644 index 0000000000..c7fa893975 --- /dev/null +++ b/frontend/__tests__/hooks/use-document-title-from-state.test.tsx @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state"; +import { useActiveConversation } from "#/hooks/query/use-active-conversation"; + +// Mock the useActiveConversation hook +vi.mock("#/hooks/query/use-active-conversation"); + +const mockUseActiveConversation = vi.mocked(useActiveConversation); + +describe("useDocumentTitleFromState", () => { + const originalTitle = document.title; + + beforeEach(() => { + vi.clearAllMocks(); + document.title = "Test"; + }); + + afterEach(() => { + document.title = originalTitle; + vi.resetAllMocks(); + }); + + it("should set document title to default suffix when no conversation", () => { + mockUseActiveConversation.mockReturnValue({ + data: null, + } as any); + + renderHook(() => useDocumentTitleFromState()); + + expect(document.title).toBe("OpenHands"); + }); + + it("should set document title to custom suffix when no conversation", () => { + mockUseActiveConversation.mockReturnValue({ + data: null, + } as any); + + renderHook(() => useDocumentTitleFromState("Custom App")); + + expect(document.title).toBe("Custom App"); + }); + + it("should set document title with conversation title", () => { + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: "My Conversation", + status: "RUNNING", + }, + } as any); + + renderHook(() => useDocumentTitleFromState()); + + expect(document.title).toBe("My Conversation | OpenHands"); + }); + + it("should update document title when conversation title changes", () => { + // Initial state - no conversation + mockUseActiveConversation.mockReturnValue({ + data: null, + } as any); + + const { rerender } = renderHook(() => useDocumentTitleFromState()); + expect(document.title).toBe("OpenHands"); + + // Conversation with initial title + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: "Conversation 65e29", + status: "RUNNING", + }, + } as any); + rerender(); + expect(document.title).toBe("Conversation 65e29 | OpenHands"); + + // Conversation title updated to human-readable title + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: "Help me build a React app", + status: "RUNNING", + }, + } as any); + rerender(); + expect(document.title).toBe("Help me build a React app | OpenHands"); + }); + + it("should handle conversation without title", () => { + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: undefined, + status: "RUNNING", + }, + } as any); + + renderHook(() => useDocumentTitleFromState()); + + expect(document.title).toBe("OpenHands"); + }); + + it("should handle empty conversation title", () => { + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: "", + status: "RUNNING", + }, + } as any); + + renderHook(() => useDocumentTitleFromState()); + + expect(document.title).toBe("OpenHands"); + }); + + it("should reset document title on cleanup", () => { + mockUseActiveConversation.mockReturnValue({ + data: { + conversation_id: "123", + title: "My Conversation", + status: "RUNNING", + }, + } as any); + + const { unmount } = renderHook(() => useDocumentTitleFromState()); + + expect(document.title).toBe("My Conversation | OpenHands"); + + unmount(); + + expect(document.title).toBe("OpenHands"); + }); +}); diff --git a/frontend/src/hooks/use-document-title-from-state.ts b/frontend/src/hooks/use-document-title-from-state.ts index 3882debb26..912c4eb1de 100644 --- a/frontend/src/hooks/use-document-title-from-state.ts +++ b/frontend/src/hooks/use-document-title-from-state.ts @@ -22,5 +22,5 @@ export function useDocumentTitleFromState(suffix = "OpenHands") { return () => { document.title = suffix; }; - }, [conversation, suffix]); + }, [conversation?.title, suffix]); }