|
|
@@ -1,337 +1,62 @@
|
|
|
-// cd webview-ui && npx jest src/components/history/__tests__/HistoryView.test.ts
|
|
|
-
|
|
|
-import { render, screen, fireEvent, within, act } from "@testing-library/react"
|
|
|
+import { render, screen, fireEvent } from "@testing-library/react"
|
|
|
import HistoryView from "../HistoryView"
|
|
|
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|
|
-import { vscode } from "@src/utils/vscode"
|
|
|
|
|
|
jest.mock("@src/context/ExtensionStateContext")
|
|
|
jest.mock("@src/utils/vscode")
|
|
|
-jest.mock("@src/i18n/TranslationContext")
|
|
|
-jest.mock("@/components/ui/checkbox", () => ({
|
|
|
- Checkbox: jest.fn(({ checked, onCheckedChange, ...props }) => (
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- data-testid={props["data-testid"] || "mock-checkbox"}
|
|
|
- checked={checked}
|
|
|
- onChange={(e) => onCheckedChange(e.target.checked)}
|
|
|
- {...props}
|
|
|
- />
|
|
|
- )),
|
|
|
-}))
|
|
|
-jest.mock("lucide-react", () => ({
|
|
|
- DollarSign: () => <span data-testid="dollar-sign">$</span>,
|
|
|
-}))
|
|
|
-jest.mock("react-virtuoso", () => ({
|
|
|
- Virtuoso: ({ data, itemContent }: any) => (
|
|
|
- <div data-testid="virtuoso-container">
|
|
|
- {data.map((item: any, index: number) => (
|
|
|
- <div key={item.id} data-testid={`virtuoso-item-${item.id}`}>
|
|
|
- {itemContent(index, item)}
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- ),
|
|
|
+jest.mock("@src/i18n/TranslationContext", () => ({
|
|
|
+ useAppTranslation: () => ({
|
|
|
+ t: (key: string) => key,
|
|
|
+ }),
|
|
|
}))
|
|
|
|
|
|
const mockTaskHistory = [
|
|
|
{
|
|
|
id: "1",
|
|
|
- number: 0,
|
|
|
task: "Test task 1",
|
|
|
- ts: new Date("2022-02-16T00:00:00").getTime(),
|
|
|
+ ts: Date.now(),
|
|
|
tokensIn: 100,
|
|
|
tokensOut: 50,
|
|
|
totalCost: 0.002,
|
|
|
+ workspace: "/test/workspace",
|
|
|
},
|
|
|
{
|
|
|
id: "2",
|
|
|
- number: 0,
|
|
|
task: "Test task 2",
|
|
|
- ts: new Date("2022-02-17T00:00:00").getTime(),
|
|
|
+ ts: Date.now() + 1000,
|
|
|
tokensIn: 200,
|
|
|
tokensOut: 100,
|
|
|
- cacheWrites: 50,
|
|
|
- cacheReads: 25,
|
|
|
+ totalCost: 0.003,
|
|
|
+ workspace: "/test/workspace",
|
|
|
},
|
|
|
]
|
|
|
|
|
|
describe("HistoryView", () => {
|
|
|
- beforeAll(() => {
|
|
|
- jest.useFakeTimers()
|
|
|
- })
|
|
|
-
|
|
|
- afterAll(() => {
|
|
|
- jest.useRealTimers()
|
|
|
- })
|
|
|
-
|
|
|
beforeEach(() => {
|
|
|
jest.clearAllMocks()
|
|
|
;(useExtensionState as jest.Mock).mockReturnValue({
|
|
|
taskHistory: mockTaskHistory,
|
|
|
+ cwd: "/test/workspace",
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- it("renders history items correctly", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Check if both tasks are rendered
|
|
|
- expect(screen.getByTestId("virtuoso-item-1")).toBeInTheDocument()
|
|
|
- expect(screen.getByTestId("virtuoso-item-2")).toBeInTheDocument()
|
|
|
- expect(screen.getByText("Test task 1")).toBeInTheDocument()
|
|
|
- expect(screen.getByText("Test task 2")).toBeInTheDocument()
|
|
|
- })
|
|
|
-
|
|
|
- it("handles search functionality", () => {
|
|
|
- // Setup clipboard mock that resolves immediately
|
|
|
- const mockClipboard = {
|
|
|
- writeText: jest.fn().mockResolvedValue(undefined),
|
|
|
- }
|
|
|
- Object.assign(navigator, { clipboard: mockClipboard })
|
|
|
-
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Get search input and radio group
|
|
|
- const searchInput = screen.getByTestId("history-search-input")
|
|
|
- const radioGroup = screen.getByRole("radiogroup")
|
|
|
-
|
|
|
- // Type in search
|
|
|
- fireEvent.input(searchInput, { target: { value: "task 1" } })
|
|
|
-
|
|
|
- // Advance timers to process search state update
|
|
|
- jest.advanceTimersByTime(100)
|
|
|
-
|
|
|
- // Check if sort option automatically changes to "Most Relevant"
|
|
|
- const mostRelevantRadio = within(radioGroup).getByTestId("radio-most-relevant")
|
|
|
- expect(mostRelevantRadio).not.toBeDisabled()
|
|
|
-
|
|
|
- // Click the radio button
|
|
|
- fireEvent.click(mostRelevantRadio)
|
|
|
-
|
|
|
- // Advance timers to process radio button state update
|
|
|
- jest.advanceTimersByTime(100)
|
|
|
-
|
|
|
- // Verify radio button is checked
|
|
|
- const updatedRadio = within(radioGroup).getByTestId("radio-most-relevant")
|
|
|
- expect(updatedRadio).toBeInTheDocument()
|
|
|
-
|
|
|
- // Verify copy the plain text content of the task when the copy button is clicked
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
- fireEvent.mouseEnter(taskContainer)
|
|
|
- const copyButton = within(taskContainer).getByTestId("copy-prompt-button")
|
|
|
- fireEvent.click(copyButton)
|
|
|
- const taskContent = within(taskContainer).getByTestId("task-content")
|
|
|
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(taskContent.textContent)
|
|
|
- })
|
|
|
-
|
|
|
- it("handles sort options correctly", async () => {
|
|
|
+ it("renders the history interface", () => {
|
|
|
const onDone = jest.fn()
|
|
|
render(<HistoryView onDone={onDone} />)
|
|
|
|
|
|
- const radioGroup = screen.getByRole("radiogroup")
|
|
|
-
|
|
|
- // Test changing sort options
|
|
|
- const oldestRadio = within(radioGroup).getByTestId("radio-oldest")
|
|
|
- fireEvent.click(oldestRadio)
|
|
|
-
|
|
|
- // Wait for oldest radio to be checked
|
|
|
- const checkedOldestRadio = within(radioGroup).getByTestId("radio-oldest")
|
|
|
- expect(checkedOldestRadio).toBeInTheDocument()
|
|
|
-
|
|
|
- const mostExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
|
|
|
- fireEvent.click(mostExpensiveRadio)
|
|
|
-
|
|
|
- // Wait for most expensive radio to be checked
|
|
|
- const checkedExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
|
|
|
- expect(checkedExpensiveRadio).toBeInTheDocument()
|
|
|
+ // Check for main UI elements
|
|
|
+ expect(screen.getByText("history:history")).toBeInTheDocument()
|
|
|
+ expect(screen.getByText("history:done")).toBeInTheDocument()
|
|
|
+ expect(screen.getByPlaceholderText("history:searchPlaceholder")).toBeInTheDocument()
|
|
|
})
|
|
|
|
|
|
- it("handles task selection", () => {
|
|
|
+ it("calls onDone when done button is clicked", () => {
|
|
|
const onDone = jest.fn()
|
|
|
render(<HistoryView onDone={onDone} />)
|
|
|
|
|
|
- // Click on first task
|
|
|
- fireEvent.click(screen.getByText("Test task 1"))
|
|
|
-
|
|
|
- // Verify vscode message was sent
|
|
|
- expect(vscode.postMessage).toHaveBeenCalledWith({
|
|
|
- type: "showTaskWithId",
|
|
|
- text: "1",
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- it("handles selection mode clicks", async () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
+ const doneButton = screen.getByText("history:done")
|
|
|
+ fireEvent.click(doneButton)
|
|
|
|
|
|
- // Go to selection mode
|
|
|
- fireEvent.click(screen.getByTestId("toggle-selection-mode-button"))
|
|
|
-
|
|
|
- const taskContainer = screen.getByTestId("task-item-1")
|
|
|
-
|
|
|
- // Click anywhere in the task item
|
|
|
- fireEvent.click(taskContainer)
|
|
|
-
|
|
|
- // Check the box instead of sending a message to open the task
|
|
|
- expect(within(taskContainer).getByRole("checkbox")).toBeChecked()
|
|
|
- expect(vscode.postMessage).not.toHaveBeenCalled()
|
|
|
- })
|
|
|
-
|
|
|
- describe("task deletion", () => {
|
|
|
- it("shows confirmation dialog on regular click", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find and hover over first task
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
- fireEvent.mouseEnter(taskContainer)
|
|
|
-
|
|
|
- // Click delete button to open confirmation dialog
|
|
|
- const deleteButton = within(taskContainer).getByTestId("delete-task-button")
|
|
|
- fireEvent.click(deleteButton)
|
|
|
-
|
|
|
- // Verify dialog is shown
|
|
|
- const dialog = screen.getByRole("alertdialog")
|
|
|
- expect(dialog).toBeInTheDocument()
|
|
|
-
|
|
|
- // Find and click the confirm delete button in the dialog
|
|
|
- const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
|
|
|
- fireEvent.click(confirmDeleteButton)
|
|
|
-
|
|
|
- // Verify vscode message was sent
|
|
|
- expect(vscode.postMessage).toHaveBeenCalledWith({
|
|
|
- type: "deleteTaskWithId",
|
|
|
- text: "1",
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- it("deletes immediately on shift-click without confirmation", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find and hover over first task
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
- fireEvent.mouseEnter(taskContainer)
|
|
|
-
|
|
|
- // Shift-click delete button
|
|
|
- const deleteButton = within(taskContainer).getByTestId("delete-task-button")
|
|
|
- fireEvent.click(deleteButton, { shiftKey: true })
|
|
|
-
|
|
|
- // Verify no dialog is shown
|
|
|
- expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
|
|
|
-
|
|
|
- // Verify vscode message was sent
|
|
|
- expect(vscode.postMessage).toHaveBeenCalledWith({
|
|
|
- type: "deleteTaskWithId",
|
|
|
- text: "1",
|
|
|
- })
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- it("handles task copying", async () => {
|
|
|
- // Setup clipboard mock that resolves immediately
|
|
|
- const mockClipboard = {
|
|
|
- writeText: jest.fn().mockResolvedValue(undefined),
|
|
|
- }
|
|
|
- Object.assign(navigator, { clipboard: mockClipboard })
|
|
|
-
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find and hover over first task
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
- fireEvent.mouseEnter(taskContainer)
|
|
|
-
|
|
|
- const copyButton = within(taskContainer).getByTestId("copy-prompt-button")
|
|
|
-
|
|
|
- // Click the copy button and wait for clipboard operation
|
|
|
- await act(async () => {
|
|
|
- fireEvent.click(copyButton)
|
|
|
- // Let the clipboard Promise resolve
|
|
|
- await Promise.resolve()
|
|
|
- // Let React process the first state update
|
|
|
- await Promise.resolve()
|
|
|
- })
|
|
|
-
|
|
|
- // Verify clipboard was called
|
|
|
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Test task 1")
|
|
|
-
|
|
|
- // Advance timer to trigger the setTimeout for modal disappearance
|
|
|
- act(() => {
|
|
|
- jest.advanceTimersByTime(2000)
|
|
|
- })
|
|
|
-
|
|
|
- // Verify modal is gone
|
|
|
- expect(screen.queryByText("Prompt Copied to Clipboard")).not.toBeInTheDocument()
|
|
|
- })
|
|
|
-
|
|
|
- it("formats dates correctly", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find first task container and check date format
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
- // Date is directly in TaskItemHeader, which is a child of TaskItem (rendered by virtuoso)
|
|
|
- const dateElement = within(taskContainer).getByText((content, element) => {
|
|
|
- if (!element) {
|
|
|
- return false
|
|
|
- }
|
|
|
- const parent = element.parentElement
|
|
|
- if (!parent) {
|
|
|
- return false
|
|
|
- }
|
|
|
- return (
|
|
|
- element.tagName.toLowerCase() === "span" &&
|
|
|
- parent.classList.contains("flex") &&
|
|
|
- parent.classList.contains("items-center") &&
|
|
|
- content.includes("FEBRUARY 16") &&
|
|
|
- content.includes("12:00 AM")
|
|
|
- )
|
|
|
- })
|
|
|
- expect(dateElement).toBeInTheDocument()
|
|
|
- })
|
|
|
-
|
|
|
- it("displays token counts correctly", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find first task container
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-1")
|
|
|
-
|
|
|
- // Find token counts within the task container (TaskItem -> TaskItemFooter)
|
|
|
- expect(within(taskContainer).getByTestId("tokens-in-footer-full")).toHaveTextContent("100")
|
|
|
- expect(within(taskContainer).getByTestId("tokens-out-footer-full")).toHaveTextContent("50")
|
|
|
- })
|
|
|
-
|
|
|
- it("displays cache information when available", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find second task container
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-2")
|
|
|
-
|
|
|
- // Find cache info within the task container (TaskItem -> TaskItemHeader)
|
|
|
- expect(within(taskContainer).getByTestId("cache-writes")).toHaveTextContent("50") // No plus sign in formatLargeNumber
|
|
|
- expect(within(taskContainer).getByTestId("cache-reads")).toHaveTextContent("25")
|
|
|
- })
|
|
|
-
|
|
|
- it("handles export functionality", () => {
|
|
|
- const onDone = jest.fn()
|
|
|
- render(<HistoryView onDone={onDone} />)
|
|
|
-
|
|
|
- // Find and hover over second task
|
|
|
- const taskContainer = screen.getByTestId("virtuoso-item-2")
|
|
|
- fireEvent.mouseEnter(taskContainer)
|
|
|
-
|
|
|
- const exportButton = within(taskContainer).getByTestId("export")
|
|
|
- fireEvent.click(exportButton)
|
|
|
-
|
|
|
- // Verify vscode message was sent
|
|
|
- expect(vscode.postMessage).toHaveBeenCalledWith({
|
|
|
- type: "exportTaskWithId",
|
|
|
- text: "2",
|
|
|
- })
|
|
|
+ expect(onDone).toHaveBeenCalled()
|
|
|
})
|
|
|
})
|