import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import { LogsTable, LogTableRow } from "./logs-table";
import { PaginationStatus } from "@/lib/types";
// Mock next/navigation
const mockPush = jest.fn();
jest.mock("next/navigation", () => ({
useRouter: () => ({
push: mockPush,
}),
}));
// Mock helper functions
jest.mock("@/lib/truncate-text");
// Import the mocked functions
import { truncateText as originalTruncateText } from "@/lib/truncate-text";
// Cast to jest.Mock for typings
const truncateText = originalTruncateText as jest.Mock;
describe("LogsTable", () => {
const defaultProps = {
data: [] as LogTableRow[],
status: "idle" as PaginationStatus,
error: null,
caption: "Test table caption",
emptyMessage: "No data found",
};
beforeEach(() => {
// Reset all mocks before each test
mockPush.mockClear();
truncateText.mockClear();
// Default pass-through implementation
truncateText.mockImplementation((text: string | undefined) => text);
});
test("renders without crashing with default props", () => {
render();
expect(screen.getByText("No data found")).toBeInTheDocument();
});
test("click on a row navigates to the correct URL", () => {
const mockData: LogTableRow[] = [
{
id: "row_123",
input: "Test input",
output: "Test output",
model: "test-model",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path/row_123",
},
];
render();
const row = screen.getByText("Test input").closest("tr");
if (row) {
fireEvent.click(row);
expect(mockPush).toHaveBeenCalledWith("/test/path/row_123");
} else {
throw new Error('Row with "Test input" not found for router mock test.');
}
});
describe("Loading State", () => {
test("renders skeleton UI when isLoading is true", () => {
const { container } = render(
,
);
// Check for skeleton in the table caption
const tableCaption = container.querySelector("caption");
expect(tableCaption).toBeInTheDocument();
if (tableCaption) {
const captionSkeleton = tableCaption.querySelector(
'[data-slot="skeleton"]',
);
expect(captionSkeleton).toBeInTheDocument();
}
// Check for skeletons in the table body cells
const tableBody = container.querySelector("tbody");
expect(tableBody).toBeInTheDocument();
if (tableBody) {
const bodySkeletons = tableBody.querySelectorAll(
'[data-slot="skeleton"]',
);
expect(bodySkeletons.length).toBeGreaterThan(0);
}
// Check that table headers are still rendered
expect(screen.getByText("Input")).toBeInTheDocument();
expect(screen.getByText("Output")).toBeInTheDocument();
expect(screen.getByText("Model")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
});
test("renders correct number of skeleton rows", () => {
const { container } = render(
,
);
const skeletonRows = container.querySelectorAll("tbody tr");
expect(skeletonRows.length).toBe(3); // Should render 3 skeleton rows
});
});
describe("Error State", () => {
test("renders error message when error prop is provided", () => {
const errorMessage = "Network Error";
render(
,
);
expect(
screen.getByText("Unable to load chat completions"),
).toBeInTheDocument();
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
test("renders default error message when error.message is not available", () => {
render(
,
);
expect(
screen.getByText("Unable to load chat completions"),
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
).toBeInTheDocument();
});
test("renders default error message when error prop is an object without message", () => {
render(
,
);
expect(
screen.getByText("Unable to load chat completions"),
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
).toBeInTheDocument();
});
test("does not render table when in error state", () => {
render(
,
);
const table = screen.queryByRole("table");
expect(table).not.toBeInTheDocument();
});
});
describe("Empty State", () => {
test("renders custom empty message when data array is empty", () => {
render(
,
);
expect(screen.getByText("Custom empty message")).toBeInTheDocument();
// Ensure that the table structure is NOT rendered in the empty state
const table = screen.queryByRole("table");
expect(table).not.toBeInTheDocument();
});
});
describe("Data Rendering", () => {
test("renders table caption, headers, and data correctly", () => {
const mockData: LogTableRow[] = [
{
id: "row_1",
input: "First input",
output: "First output",
model: "model-1",
createdTime: "2024-01-01 12:00:00",
detailPath: "/path/1",
},
{
id: "row_2",
input: "Second input",
output: "Second output",
model: "model-2",
createdTime: "2024-01-02 13:00:00",
detailPath: "/path/2",
},
];
render(
,
);
// Table caption
expect(screen.getByText("Custom table caption")).toBeInTheDocument();
// Table headers
expect(screen.getByText("Input")).toBeInTheDocument();
expect(screen.getByText("Output")).toBeInTheDocument();
expect(screen.getByText("Model")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
// Data rows
expect(screen.getByText("First input")).toBeInTheDocument();
expect(screen.getByText("First output")).toBeInTheDocument();
expect(screen.getByText("model-1")).toBeInTheDocument();
expect(screen.getByText("2024-01-01 12:00:00")).toBeInTheDocument();
expect(screen.getByText("Second input")).toBeInTheDocument();
expect(screen.getByText("Second output")).toBeInTheDocument();
expect(screen.getByText("model-2")).toBeInTheDocument();
expect(screen.getByText("2024-01-02 13:00:00")).toBeInTheDocument();
});
test("applies correct CSS classes to table rows", () => {
const mockData: LogTableRow[] = [
{
id: "row_1",
input: "Test input",
output: "Test output",
model: "test-model",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path",
},
];
render();
const row = screen.getByText("Test input").closest("tr");
expect(row).toHaveClass("cursor-pointer");
expect(row).toHaveClass("hover:bg-muted/50");
});
test("applies correct alignment to Created column", () => {
const mockData: LogTableRow[] = [
{
id: "row_1",
input: "Test input",
output: "Test output",
model: "test-model",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path",
},
];
render();
const createdCell = screen.getByText("2024-01-01 12:00:00").closest("td");
expect(createdCell).toHaveClass("text-right");
});
});
describe("Text Truncation", () => {
test("truncates input and output text using truncateText function", () => {
// Mock truncateText to return truncated versions
truncateText.mockImplementation((text: string | undefined) => {
if (typeof text === "string" && text.length > 10) {
return text.slice(0, 10) + "...";
}
return text;
});
const longInput =
"This is a very long input text that should be truncated";
const longOutput =
"This is a very long output text that should be truncated";
const mockData: LogTableRow[] = [
{
id: "row_1",
input: longInput,
output: longOutput,
model: "test-model",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path",
},
];
render();
// Verify truncateText was called
expect(truncateText).toHaveBeenCalledWith(longInput);
expect(truncateText).toHaveBeenCalledWith(longOutput);
// Verify truncated text is displayed
const truncatedTexts = screen.getAllByText("This is a ...");
expect(truncatedTexts).toHaveLength(2); // one for input, one for output
truncatedTexts.forEach((textElement) =>
expect(textElement).toBeInTheDocument(),
);
});
test("does not truncate model names", () => {
const mockData: LogTableRow[] = [
{
id: "row_1",
input: "Test input",
output: "Test output",
model: "very-long-model-name-that-should-not-be-truncated",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path",
},
];
render();
// Model name should not be passed to truncateText
expect(truncateText).not.toHaveBeenCalledWith(
"very-long-model-name-that-should-not-be-truncated",
);
// Full model name should be displayed
expect(
screen.getByText("very-long-model-name-that-should-not-be-truncated"),
).toBeInTheDocument();
});
});
describe("Accessibility", () => {
test("table has proper role and structure", () => {
const mockData: LogTableRow[] = [
{
id: "row_1",
input: "Test input",
output: "Test output",
model: "test-model",
createdTime: "2024-01-01 12:00:00",
detailPath: "/test/path",
},
];
render();
const tables = screen.getAllByRole("table");
expect(tables).toHaveLength(2); // Fixed header table + body table
const columnHeaders = screen.getAllByRole("columnheader");
expect(columnHeaders).toHaveLength(4);
const rows = screen.getAllByRole("row");
expect(rows).toHaveLength(3); // 1 header row + 1 data row + 1 "no more items" row
expect(screen.getByText("Input")).toBeInTheDocument();
expect(screen.getByText("Output")).toBeInTheDocument();
expect(screen.getByText("Model")).toBeInTheDocument();
expect(screen.getByText("Created")).toBeInTheDocument();
});
});
});