feat(frontend): add refresh button to changes tab (#12036)

Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
This commit is contained in:
Hiep Le 2025-12-17 22:29:18 +07:00 committed by GitHub
parent 2c83e419dc
commit 0607614372
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 209 additions and 2 deletions

View File

@ -0,0 +1,149 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConversationTabTitle } from "#/components/features/conversation/conversation-tabs/conversation-tab-title";
import GitService from "#/api/git-service/git-service.api";
import V1GitService from "#/api/git-service/v1-git-service.api";
// Mock the services that the hook depends on
vi.mock("#/api/git-service/git-service.api");
vi.mock("#/api/git-service/v1-git-service.api");
// Mock the hooks that useUnifiedGetGitChanges depends on
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({
conversationId: "test-conversation-id",
}),
}));
vi.mock("#/hooks/query/use-active-conversation", () => ({
useActiveConversation: () => ({
data: {
conversation_version: "V0",
url: null,
session_api_key: null,
selected_repository: null,
},
}),
}));
vi.mock("#/hooks/use-runtime-is-ready", () => ({
useRuntimeIsReady: () => true,
}));
vi.mock("#/utils/get-git-path", () => ({
getGitPath: () => "/workspace",
}));
describe("ConversationTabTitle", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// Mock GitService methods
vi.mocked(GitService.getGitChanges).mockResolvedValue([]);
vi.mocked(V1GitService.getGitChanges).mockResolvedValue([]);
});
afterEach(() => {
vi.clearAllMocks();
queryClient.clear();
});
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
);
};
describe("Rendering", () => {
it("should render the title", () => {
// Arrange
const title = "Test Title";
// Act
renderWithProviders(
<ConversationTabTitle title={title} conversationKey="browser" />,
);
// Assert
expect(screen.getByText(title)).toBeInTheDocument();
});
it("should show refresh button when conversationKey is 'editor'", () => {
// Arrange
const title = "Changes";
// Act
renderWithProviders(
<ConversationTabTitle title={title} conversationKey="editor" />,
);
// Assert
const refreshButton = screen.getByRole("button");
expect(refreshButton).toBeInTheDocument();
});
it("should not show refresh button when conversationKey is not 'editor'", () => {
// Arrange
const title = "Browser";
// Act
renderWithProviders(
<ConversationTabTitle title={title} conversationKey="browser" />,
);
// Assert
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
});
describe("User Interactions", () => {
it("should call refetch and trigger GitService.getGitChanges when refresh button is clicked", async () => {
// Arrange
const user = userEvent.setup();
const title = "Changes";
const mockGitChanges: Array<{
path: string;
status: "M" | "A" | "D" | "R" | "U";
}> = [
{ path: "file1.ts", status: "M" },
{ path: "file2.ts", status: "A" },
];
vi.mocked(GitService.getGitChanges).mockResolvedValue(mockGitChanges);
renderWithProviders(
<ConversationTabTitle title={title} conversationKey="editor" />,
);
const refreshButton = screen.getByRole("button");
// Wait for initial query to complete
await waitFor(() => {
expect(GitService.getGitChanges).toHaveBeenCalled();
});
// Clear the mock to track refetch calls
vi.mocked(GitService.getGitChanges).mockClear();
// Act
await user.click(refreshButton);
// Assert - refetch should trigger another service call
await waitFor(() => {
expect(GitService.getGitChanges).toHaveBeenCalledWith(
"test-conversation-id",
);
});
});
});
});

View File

@ -82,13 +82,45 @@ export function ConversationTabContent() {
isPlannerActive,
]);
const conversationKey = useMemo(() => {
if (isEditorActive) {
return "editor";
}
if (isBrowserActive) {
return "browser";
}
if (isServedActive) {
return "served";
}
if (isVSCodeActive) {
return "vscode";
}
if (isTerminalActive) {
return "terminal";
}
if (isPlannerActive) {
return "planner";
}
return "";
}, [
isEditorActive,
isBrowserActive,
isServedActive,
isVSCodeActive,
isTerminalActive,
isPlannerActive,
]);
if (shouldShownAgentLoading) {
return <ConversationLoading />;
}
return (
<TabContainer>
<ConversationTabTitle title={conversationTabTitle} />
<ConversationTabTitle
title={conversationTabTitle}
conversationKey={conversationKey}
/>
<TabContentArea>
{tabs.map(({ key, component: Component, isActive }) => (
<TabWrapper

View File

@ -1,11 +1,33 @@
import RefreshIcon from "#/icons/u-refresh.svg?react";
import { useUnifiedGetGitChanges } from "#/hooks/query/use-unified-get-git-changes";
type ConversationTabTitleProps = {
title: string;
conversationKey: string;
};
export function ConversationTabTitle({ title }: ConversationTabTitleProps) {
export function ConversationTabTitle({
title,
conversationKey,
}: ConversationTabTitleProps) {
const { refetch } = useUnifiedGetGitChanges();
const handleRefresh = () => {
refetch();
};
return (
<div className="flex flex-row items-center justify-between border-b border-[#474A54] py-2 px-3">
<span className="text-xs font-medium text-white">{title}</span>
{conversationKey === "editor" && (
<button
type="button"
className="flex w-[26px] py-1 justify-center items-center gap-[10px] rounded-[7px] hover:bg-[#474A54] cursor-pointer"
onClick={handleRefresh}
>
<RefreshIcon width={12.75} height={15} color="#ffffff" />
</button>
)}
</div>
);
}

View File

@ -103,5 +103,6 @@ export const useUnifiedGetGitChanges = () => {
isSuccess: result.isSuccess,
isError: result.isError,
error: result.error,
refetch: result.refetch,
};
};

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 13 15" fill="none">
<path d="M6.59467 0.21967C6.88756 -0.0732233 7.36244 -0.0732233 7.65533 0.21967L9.90533 2.46967C10.1982 2.76256 10.1982 3.23744 9.90533 3.53033L7.65533 5.78033C7.36244 6.07322 6.88756 6.07322 6.59467 5.78033C6.30178 5.48744 6.30178 5.01256 6.59467 4.71967L7.56434 3.75H6.375C3.71421 3.75 1.5 5.96421 1.5 8.625C1.5 11.2858 3.71421 13.5 6.375 13.5C9.03579 13.5 11.25 11.2858 11.25 8.625C11.25 8.21079 11.5858 7.875 12 7.875C12.4142 7.875 12.75 8.21079 12.75 8.625C12.75 12.1142 9.86421 15 6.375 15C2.88579 15 0 12.1142 0 8.625C0 5.13579 2.88579 2.25 6.375 2.25H7.56434L6.59467 1.28033C6.30178 0.987437 6.30178 0.512563 6.59467 0.21967Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 762 B