mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
feat: add chat message skeletons and improve routing stability (#12223)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
This commit is contained in:
@@ -10,13 +10,14 @@ import {
|
||||
} from "vitest";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { MemoryRouter, Route, Routes } from "react-router";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { renderWithProviders, useParamsMock } from "test-utils";
|
||||
import type { Message } from "#/message";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { ChatInterface } from "#/components/features/chat/chat-interface";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
@@ -31,19 +32,8 @@ vi.mock("#/context/ws-client-provider");
|
||||
vi.mock("#/hooks/query/use-config");
|
||||
vi.mock("#/hooks/mutation/use-get-trajectory");
|
||||
vi.mock("#/hooks/mutation/use-unified-upload-files");
|
||||
vi.mock("#/hooks/use-conversation-id");
|
||||
|
||||
// Mock React Router hooks at the top level
|
||||
vi.mock("react-router", async () => {
|
||||
const actual = await vi.importActual("react-router");
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => vi.fn(),
|
||||
useParams: () => ({ conversationId: "test-conversation-id" }),
|
||||
useRouteLoaderData: vi.fn(() => ({})),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock other hooks that might be used by the component
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: () => ({
|
||||
providers: [],
|
||||
@@ -87,13 +77,26 @@ const renderChatInterface = (messages: Message[]) =>
|
||||
const renderWithQueryClient = (
|
||||
ui: React.ReactElement,
|
||||
queryClient: QueryClient,
|
||||
route = "/test-conversation-id",
|
||||
) =>
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>{ui}</MemoryRouter>
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<Routes>
|
||||
<Route path="/:conversationId" element={ui} />
|
||||
<Route path="/" element={ui} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
useParamsMock.mockReturnValue({ conversationId: "test-conversation-id" });
|
||||
vi.mocked(useConversationId).mockReturnValue({
|
||||
conversationId: "test-conversation-id",
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatInterface - Chat Suggestions", () => {
|
||||
// Create a new QueryClient for each test
|
||||
let queryClient: QueryClient;
|
||||
@@ -129,7 +132,9 @@ describe("ChatInterface - Chat Suggestions", () => {
|
||||
mutateAsync: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
(useUnifiedUploadFiles as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
(
|
||||
useUnifiedUploadFiles as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
mutateAsync: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ skipped_files: [], uploaded_files: [] }),
|
||||
@@ -260,7 +265,9 @@ describe("ChatInterface - Empty state", () => {
|
||||
mutateAsync: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
(useUnifiedUploadFiles as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
(
|
||||
useUnifiedUploadFiles as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
mutateAsync: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ skipped_files: [], uploaded_files: [] }),
|
||||
@@ -635,3 +642,43 @@ describe.skip("ChatInterface - General functionality", () => {
|
||||
expect(screen.getByTestId("feedback-actions")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatInterface – skeleton loading state", () => {
|
||||
test("renders chat message skeleton when loading existing conversation", () => {
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: true,
|
||||
parsedEvents: [],
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />, new QueryClient());
|
||||
|
||||
expect(screen.getByTestId("chat-messages-skeleton")).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId("chat-suggestions")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("does not render skeleton for new conversation (shows spinner instead)", () => {
|
||||
useParamsMock.mockReturnValue({ conversationId: undefined } as unknown as {
|
||||
conversationId: string;
|
||||
});
|
||||
(useConversationId as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
conversationId: "",
|
||||
});
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: true,
|
||||
parsedEvents: [],
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />, new QueryClient(), "/");
|
||||
|
||||
expect(screen.getAllByTestId("loading-spinner").length).toBeGreaterThan(0);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("chat-messages-skeleton"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { screen, waitFor, render, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { MemoryRouter, Route, Routes } from "react-router";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
@@ -78,13 +79,22 @@ function renderWithWebSocketContext(
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConversationWebSocketProvider
|
||||
conversationId={conversationId}
|
||||
conversationUrl={conversationUrl}
|
||||
sessionApiKey={sessionApiKey}
|
||||
>
|
||||
{children}
|
||||
</ConversationWebSocketProvider>
|
||||
<MemoryRouter initialEntries={["/test-conversation-default"]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:conversationId"
|
||||
element={
|
||||
<ConversationWebSocketProvider
|
||||
conversationId={conversationId}
|
||||
conversationUrl={conversationUrl}
|
||||
sessionApiKey={sessionApiKey}
|
||||
>
|
||||
{children}
|
||||
</ConversationWebSocketProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user