Adding unit test to expand unit testing coverage

This commit is contained in:
yuneng-jiang 2026-01-08 20:23:54 -08:00
parent c81ac6cf46
commit bcbf8d1de3
4 changed files with 778 additions and 0 deletions

View file

@ -0,0 +1,187 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import {
convertImageToBase64,
createChatMultimodalMessage,
createChatDisplayMessage,
shouldShowChatAttachedImage,
} from "./ChatImageUtils";
import { MessageType } from "./types";
describe("ChatImageUtils", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("convertImageToBase64", () => {
it("should convert file to base64 data URI", async () => {
const file = new File(["test content"], "test.png", { type: "image/png" });
const result = await convertImageToBase64(file);
expect(result).toMatch(/^data:image\/png;base64,/);
});
it("should handle different file types", async () => {
const jpegFile = new File(["jpeg content"], "test.jpg", { type: "image/jpeg" });
const result = await convertImageToBase64(jpegFile);
expect(result).toMatch(/^data:image\/jpeg;base64,/);
});
it("should reject on file read error", async () => {
const file = new File(["test"], "test.png", { type: "image/png" });
const originalReadAsDataURL = FileReader.prototype.readAsDataURL;
FileReader.prototype.readAsDataURL = vi.fn(function (this: FileReader) {
setTimeout(() => {
if (this.onerror) {
this.onerror(new Error("Read error") as any);
}
}, 0);
});
await expect(convertImageToBase64(file)).rejects.toThrow();
FileReader.prototype.readAsDataURL = originalReadAsDataURL;
});
});
describe("createChatMultimodalMessage", () => {
it("should create multimodal message with text and image", async () => {
const file = new File(["test content"], "test.png", { type: "image/png" });
const inputMessage = "What is in this image?";
const result = await createChatMultimodalMessage(inputMessage, file);
expect(result.role).toBe("user");
expect(result.content).toHaveLength(2);
expect(result.content[0]).toEqual({ type: "text", text: inputMessage });
expect(result.content[1]).toMatchObject({
type: "image_url",
image_url: {
url: expect.stringMatching(/^data:image\/png;base64,/),
},
});
});
it("should include base64 data URI in image_url", async () => {
const file = new File(["test content"], "test.png", { type: "image/png" });
const result = await createChatMultimodalMessage("test", file);
const imageContent = result.content[1];
expect(imageContent.type).toBe("image_url");
if ("image_url" in imageContent && imageContent.image_url) {
expect(imageContent.image_url.url).toMatch(/^data:/);
}
});
});
describe("createChatDisplayMessage", () => {
it("should create display message without file", () => {
const result = createChatDisplayMessage("Hello world", false);
expect(result.role).toBe("user");
expect(result.content).toBe("Hello world");
expect(result.imagePreviewUrl).toBeUndefined();
});
it("should create display message with PDF file", () => {
const filePreviewUrl = "blob:test-url";
const result = createChatDisplayMessage("Read this", true, filePreviewUrl, "document.pdf");
expect(result.content).toBe("Read this [PDF attached]");
expect(result.imagePreviewUrl).toBe(filePreviewUrl);
});
it("should create display message with image file", () => {
const filePreviewUrl = "blob:test-url";
const result = createChatDisplayMessage("Look at this", true, filePreviewUrl, "photo.jpg");
expect(result.content).toBe("Look at this [Image attached]");
expect(result.imagePreviewUrl).toBe(filePreviewUrl);
});
it("should create display message with file but no fileName", () => {
const filePreviewUrl = "blob:test-url";
const result = createChatDisplayMessage("Check this", true, filePreviewUrl);
expect(result.content).toBe("Check this ");
expect(result.imagePreviewUrl).toBe(filePreviewUrl);
});
it("should create display message with file but no preview URL", () => {
const result = createChatDisplayMessage("See this", true, undefined, "image.png");
expect(result.content).toBe("See this [Image attached]");
expect(result.imagePreviewUrl).toBeUndefined();
});
});
describe("shouldShowChatAttachedImage", () => {
it("should return true for user message with image attachment", () => {
const message: MessageType = {
role: "user",
content: "Check this [Image attached]",
imagePreviewUrl: "blob:test-url",
};
expect(shouldShowChatAttachedImage(message)).toBe(true);
});
it("should return true for user message with PDF attachment", () => {
const message: MessageType = {
role: "user",
content: "Read this [PDF attached]",
imagePreviewUrl: "blob:test-url",
};
expect(shouldShowChatAttachedImage(message)).toBe(true);
});
it("should return false for assistant message", () => {
const message: MessageType = {
role: "assistant",
content: "Here is the image [Image attached]",
imagePreviewUrl: "blob:test-url",
};
expect(shouldShowChatAttachedImage(message)).toBe(false);
});
it("should return false when content is not a string", () => {
const message: MessageType = {
role: "user",
content: [{ type: "input_text", text: "test" }],
imagePreviewUrl: "blob:test-url",
};
expect(shouldShowChatAttachedImage(message)).toBe(false);
});
it("should return false when content does not include attachment marker", () => {
const message: MessageType = {
role: "user",
content: "Just regular text",
imagePreviewUrl: "blob:test-url",
};
expect(shouldShowChatAttachedImage(message)).toBe(false);
});
it("should return false when imagePreviewUrl is missing", () => {
const message: MessageType = {
role: "user",
content: "Check this [Image attached]",
};
expect(shouldShowChatAttachedImage(message)).toBe(false);
});
it("should return false when imagePreviewUrl is empty string", () => {
const message: MessageType = {
role: "user",
content: "Check this [Image attached]",
imagePreviewUrl: "",
};
expect(shouldShowChatAttachedImage(message)).toBe(false);
});
});
});

View file

@ -0,0 +1,326 @@
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 CodeInterpreterOutput from "./CodeInterpreterOutput";
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => "https://example.com"),
}));
global.fetch = vi.fn();
describe("CodeInterpreterOutput", () => {
beforeEach(() => {
vi.clearAllMocks();
URL.createObjectURL = vi.fn((blob) => `blob:${blob}`);
URL.revokeObjectURL = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should render", () => {
render(<CodeInterpreterOutput code="print('hello')" accessToken="test-token" />);
expect(screen.getByText("Python Code Executed")).toBeInTheDocument();
});
it("should display code in syntax highlighter", async () => {
const user = userEvent.setup();
const code = "print('hello world')";
const { container } = render(<CodeInterpreterOutput code={code} accessToken="test-token" />);
expect(screen.getByText("Python Code Executed")).toBeInTheDocument();
const collapseHeader = screen.getByRole("button");
await user.click(collapseHeader);
await waitFor(() => {
const codeElement = container.querySelector("code.language-python");
expect(codeElement).toBeInTheDocument();
expect(codeElement?.textContent).toContain(code);
});
});
it("should fetch and display images from annotations", async () => {
const mockBlob = new Blob(["image data"], { type: "image/png" });
const mockResponse = {
ok: true,
blob: vi.fn().mockResolvedValue(mockBlob),
};
(global.fetch as any).mockResolvedValue(mockResponse);
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "chart.png",
start_index: 0,
end_index: 10,
},
];
render(
<CodeInterpreterOutput
code="import matplotlib.pyplot as plt"
annotations={annotations}
accessToken="test-token"
/>,
);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/v1/containers/container-1/files/file-1/content",
expect.objectContaining({
headers: {
Authorization: "Bearer test-token",
},
}),
);
});
await waitFor(() => {
expect(screen.getByText("chart.png")).toBeInTheDocument();
});
});
it("should show loading state while fetching images", async () => {
const mockBlob = new Blob(["image data"], { type: "image/png" });
let resolveBlob: (value: Blob) => void;
const blobPromise = new Promise<Blob>((resolve) => {
resolveBlob = resolve;
});
const mockResponse = {
ok: true,
blob: vi.fn().mockReturnValue(blobPromise),
};
(global.fetch as any).mockResolvedValue(mockResponse);
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "chart.png",
start_index: 0,
end_index: 10,
},
];
render(
<CodeInterpreterOutput
code="import matplotlib.pyplot as plt"
annotations={annotations}
accessToken="test-token"
/>,
);
await waitFor(() => {
expect(screen.getByText("Loading image...")).toBeInTheDocument();
});
resolveBlob!(mockBlob);
await waitFor(() => {
expect(screen.queryByText("Loading image...")).not.toBeInTheDocument();
});
});
it("should handle download for image files", async () => {
const user = userEvent.setup();
const mockBlob = new Blob(["image data"], { type: "image/png" });
const mockResponse = {
ok: true,
blob: vi.fn().mockResolvedValue(mockBlob),
};
(global.fetch as any).mockResolvedValue(mockResponse);
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "chart.png",
start_index: 0,
end_index: 10,
},
];
const createElementSpy = vi.spyOn(document, "createElement");
const appendChildSpy = vi.spyOn(document.body, "appendChild");
const removeChildSpy = vi.spyOn(document.body, "removeChild");
render(
<CodeInterpreterOutput
code="import matplotlib.pyplot as plt"
annotations={annotations}
accessToken="test-token"
/>,
);
await waitFor(() => {
expect(screen.getByText("chart.png")).toBeInTheDocument();
});
const downloadButton = screen.getByText("Download");
await user.click(downloadButton);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/v1/containers/container-1/files/file-1/content",
expect.objectContaining({
headers: {
Authorization: "Bearer test-token",
},
}),
);
});
createElementSpy.mockRestore();
appendChildSpy.mockRestore();
removeChildSpy.mockRestore();
});
it("should handle download for non-image files", async () => {
const user = userEvent.setup();
const mockBlob = new Blob(["file data"], { type: "text/plain" });
const mockResponse = {
ok: true,
blob: vi.fn().mockResolvedValue(mockBlob),
};
(global.fetch as any).mockResolvedValue(mockResponse);
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "data.csv",
start_index: 0,
end_index: 10,
},
];
render(<CodeInterpreterOutput code="import pandas as pd" annotations={annotations} accessToken="test-token" />);
await waitFor(() => {
expect(screen.getByText("data.csv")).toBeInTheDocument();
});
const downloadButton = screen.getByText("data.csv").closest("button");
expect(downloadButton).toBeInTheDocument();
if (downloadButton) {
await user.click(downloadButton);
}
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/v1/containers/container-1/files/file-1/content",
expect.objectContaining({
headers: {
Authorization: "Bearer test-token",
},
}),
);
});
});
it("should return null when no code and no annotations", () => {
const { container } = render(<CodeInterpreterOutput accessToken="test-token" />);
expect(container.firstChild).toBeNull();
});
it("should handle multiple image formats", async () => {
const mockBlob = new Blob(["image data"], { type: "image/png" });
const mockResponse = {
ok: true,
blob: vi.fn().mockResolvedValue(mockBlob),
};
(global.fetch as any).mockResolvedValue(mockResponse);
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "image.png",
start_index: 0,
end_index: 10,
},
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-2",
filename: "image.jpg",
start_index: 0,
end_index: 10,
},
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-3",
filename: "image.jpeg",
start_index: 0,
end_index: 10,
},
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-4",
filename: "image.gif",
start_index: 0,
end_index: 10,
},
];
render(
<CodeInterpreterOutput
code="import matplotlib.pyplot as plt"
annotations={annotations}
accessToken="test-token"
/>,
);
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledTimes(4);
});
});
it("should handle fetch errors gracefully", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
(global.fetch as any).mockRejectedValue(new Error("Network error"));
const annotations = [
{
type: "container_file_citation" as const,
container_id: "container-1",
file_id: "file-1",
filename: "chart.png",
start_index: 0,
end_index: 10,
},
];
render(
<CodeInterpreterOutput
code="import matplotlib.pyplot as plt"
annotations={annotations}
accessToken="test-token"
/>,
);
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalled();
});
consoleErrorSpy.mockRestore();
});
});

View file

@ -0,0 +1,161 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { UnifiedSelector } from "./UnifiedSelector";
import { EndpointId, ENDPOINT_CONFIGS } from "../endpoint_config";
describe("UnifiedSelector", () => {
it("should render", () => {
const onChange = vi.fn();
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
render(<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />);
const select = screen.getByRole("combobox");
expect(select).toBeInTheDocument();
});
it("should display placeholder when not loading", () => {
const onChange = vi.fn();
const options = [{ value: "option1", label: "Option 1" }];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
const { container } = render(
<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />,
);
const placeholder = container.querySelector(".ant-select-selection-placeholder");
expect(placeholder).toHaveTextContent(config.selectorPlaceholder);
});
it("should display loading placeholder when loading", () => {
const onChange = vi.fn();
const options = [{ value: "option1", label: "Option 1" }];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
const { container } = render(
<UnifiedSelector value="" options={options} loading={true} config={config} onChange={onChange} />,
);
const placeholder = container.querySelector(".ant-select-selection-placeholder");
expect(placeholder).toHaveTextContent(`Loading ${config.selectorLabel.toLowerCase()}s...`);
});
it("should call onChange when option is selected", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
render(<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />);
const select = screen.getByRole("combobox");
await user.click(select);
await waitFor(() => {
const option = screen.getByText("Option 1");
expect(option).toBeInTheDocument();
});
const option = screen.getByText("Option 1");
await user.click(option);
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
});
const callArgs = onChange.mock.calls[0];
expect(callArgs[0]).toBe("option1");
});
it("should display selected value", () => {
const onChange = vi.fn();
const options = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
const { container } = render(
<UnifiedSelector value="option1" options={options} loading={false} config={config} onChange={onChange} />,
);
const selectedValue = container.querySelector(".ant-select-selection-item");
expect(selectedValue).toHaveTextContent("Option 1");
});
it("should filter options by search input", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const options = [
{ value: "option1", label: "Option One" },
{ value: "option2", label: "Option Two" },
{ value: "option3", label: "Different" },
];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
render(<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />);
const select = screen.getByRole("combobox");
await user.click(select);
await user.type(select, "One");
await waitFor(() => {
expect(screen.getByText("Option One")).toBeInTheDocument();
expect(screen.queryByText("Option Two")).not.toBeInTheDocument();
expect(screen.queryByText("Different")).not.toBeInTheDocument();
});
});
it("should show loading spinner in notFoundContent when loading", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const options: { value: string; label: string }[] = [];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
render(<UnifiedSelector value="" options={options} loading={true} config={config} onChange={onChange} />);
const select = screen.getByRole("combobox");
await user.click(select);
await waitFor(() => {
const spin = document.querySelector(".ant-spin");
expect(spin).toBeInTheDocument();
});
});
it("should show no options message when not loading and no options", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const options: { value: string; label: string }[] = [];
const config = ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS];
render(<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />);
const select = screen.getByRole("combobox");
await user.click(select);
await waitFor(() => {
expect(screen.getByText(`No ${config.selectorLabel.toLowerCase()}s available`)).toBeInTheDocument();
});
});
it("should work with agent endpoint config", () => {
const onChange = vi.fn();
const options = [{ value: "agent1", label: "Agent One" }];
const config = ENDPOINT_CONFIGS[EndpointId.A2A_AGENTS];
const { container } = render(
<UnifiedSelector value="" options={options} loading={false} config={config} onChange={onChange} />,
);
const placeholder = container.querySelector(".ant-select-selection-placeholder");
expect(placeholder).toHaveTextContent(config.selectorPlaceholder);
});
});

View file

@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";
import {
EndpointId,
ENDPOINT_CONFIGS,
getAvailableEndpoints,
getEndpointConfig,
isAgentEndpoint,
isModelEndpoint,
modelOptionsToSelectorOptions,
agentOptionsToSelectorOptions,
getSelectionFieldName,
getComparisonSelection,
hasValidSelection,
} from "./endpoint_config";
import { Agent } from "../llm_calls/fetch_agents";
describe("endpoint_config", () => {
it("should export EndpointId constants", () => {
expect(EndpointId.CHAT_COMPLETIONS).toBe("/v1/chat/completions");
expect(EndpointId.A2A_AGENTS).toBe("/a2a");
});
it("should have endpoint configs for all endpoint IDs", () => {
expect(ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS]).toBeDefined();
expect(ENDPOINT_CONFIGS[EndpointId.A2A_AGENTS]).toBeDefined();
expect(ENDPOINT_CONFIGS[EndpointId.CHAT_COMPLETIONS].selectorType).toBe("model");
expect(ENDPOINT_CONFIGS[EndpointId.A2A_AGENTS].selectorType).toBe("agent");
});
it("should get available endpoints", () => {
const endpoints = getAvailableEndpoints();
expect(endpoints).toHaveLength(2);
expect(endpoints).toContainEqual({
value: EndpointId.CHAT_COMPLETIONS,
label: "/v1/chat/completions",
});
expect(endpoints).toContainEqual({
value: EndpointId.A2A_AGENTS,
label: "/a2a (Agents)",
});
});
it("should get endpoint config by ID", () => {
const config = getEndpointConfig(EndpointId.CHAT_COMPLETIONS);
expect(config.id).toBe(EndpointId.CHAT_COMPLETIONS);
expect(config.selectorType).toBe("model");
expect(config.selectorLabel).toBe("Model");
});
it("should check if endpoint is agent endpoint", () => {
expect(isAgentEndpoint(EndpointId.A2A_AGENTS)).toBe(true);
expect(isAgentEndpoint(EndpointId.CHAT_COMPLETIONS)).toBe(false);
});
it("should check if endpoint is model endpoint", () => {
expect(isModelEndpoint(EndpointId.CHAT_COMPLETIONS)).toBe(true);
expect(isModelEndpoint(EndpointId.A2A_AGENTS)).toBe(false);
});
it("should convert model options to selector options", () => {
const models = ["gpt-4", "gpt-3.5-turbo", "claude-3"];
const options = modelOptionsToSelectorOptions(models);
expect(options).toHaveLength(3);
expect(options[0]).toEqual({ value: "gpt-4", label: "gpt-4" });
expect(options[1]).toEqual({ value: "gpt-3.5-turbo", label: "gpt-3.5-turbo" });
expect(options[2]).toEqual({ value: "claude-3", label: "claude-3" });
});
it("should convert agent options to selector options", () => {
const agents: Agent[] = [
{ agent_id: "agent-1", agent_name: "Agent One" },
{ agent_id: "agent-2", agent_name: "Agent Two" },
{ agent_id: "agent-3", agent_name: undefined as any },
];
const options = agentOptionsToSelectorOptions(agents);
expect(options).toHaveLength(3);
expect(options[0]).toEqual({ value: "Agent One", label: "Agent One" });
expect(options[1]).toEqual({ value: "Agent Two", label: "Agent Two" });
expect(options[2]).toEqual({ value: undefined, label: "agent-3" });
});
it("should get selection field name based on endpoint", () => {
expect(getSelectionFieldName(EndpointId.CHAT_COMPLETIONS)).toBe("model");
expect(getSelectionFieldName(EndpointId.A2A_AGENTS)).toBe("agent");
});
it("should get comparison selection based on endpoint", () => {
const comparison = { model: "gpt-4", agent: "agent-1" };
expect(getComparisonSelection(comparison, EndpointId.CHAT_COMPLETIONS)).toBe("gpt-4");
expect(getComparisonSelection(comparison, EndpointId.A2A_AGENTS)).toBe("agent-1");
});
it("should check if comparison has valid selection", () => {
const comparisonWithModel = { model: "gpt-4", agent: "" };
const comparisonWithAgent = { model: "", agent: "agent-1" };
const comparisonEmpty = { model: "", agent: "" };
const comparisonWhitespace = { model: " ", agent: "" };
expect(hasValidSelection(comparisonWithModel, EndpointId.CHAT_COMPLETIONS)).toBe(true);
expect(hasValidSelection(comparisonWithAgent, EndpointId.A2A_AGENTS)).toBe(true);
expect(hasValidSelection(comparisonEmpty, EndpointId.CHAT_COMPLETIONS)).toBe(false);
expect(hasValidSelection(comparisonWhitespace, EndpointId.CHAT_COMPLETIONS)).toBe(false);
});
});