Skip to content

Commit ae36388

Browse files
committed
test: history items
1 parent 84b4fae commit ae36388

13 files changed

+1007
-402
lines changed

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
9494
</div>
9595
<div className="flex flex-col gap-2">
9696
<VSCodeTextField
97-
style={{ width: "100%" }}
97+
className="w-full"
9898
placeholder={t("history:searchPlaceholder")}
9999
value={searchQuery}
100100
data-testid="history-search-input"
@@ -106,23 +106,13 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
106106
setSortOption("mostRelevant")
107107
}
108108
}}>
109-
<div
110-
slot="start"
111-
className="codicon codicon-search"
112-
style={{ fontSize: 13, marginTop: 2.5, opacity: 0.8 }}
113-
/>
109+
<div slot="start" className="codicon codicon-search mt-0.5 opacity-80 text-sm!" />
114110
{searchQuery && (
115111
<div
116-
className="input-icon-button codicon codicon-close"
112+
className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
117113
aria-label="Clear search"
118114
onClick={() => setSearchQuery("")}
119115
slot="end"
120-
style={{
121-
display: "flex",
122-
justifyContent: "center",
123-
alignItems: "center",
124-
height: "100%",
125-
}}
126116
/>
127117
)}
128118
</VSCodeTextField>
@@ -223,10 +213,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
223213

224214
<TabContent className="p-0">
225215
<Virtuoso
226-
style={{
227-
flexGrow: 1,
228-
overflowY: "scroll",
229-
}}
216+
className="flex-1 overflow-y-scroll"
230217
data={tasks}
231218
data-testid="virtuoso-container"
232219
initialTopMostItemIndex={0}

webview-ui/src/components/history/TaskItemFooter.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,24 @@ export interface TaskItemFooterProps {
1313
}
1414

1515
const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false }) => {
16-
const metadataIconWithTextAdjustStyle: React.CSSProperties = {
17-
fontSize: "12px",
18-
color: "var(--vscode-descriptionForeground)",
19-
verticalAlign: "middle",
20-
marginBottom: "-2px",
21-
fontWeight: "bold",
22-
}
23-
2416
return (
2517
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center mt-1">
2618
<div className="flex gap-2">
27-
{!!item.cacheWrites && (
28-
<span className="flex items-center gap-px" data-testid="cache-compact">
29-
<i className="codicon codicon-database" style={metadataIconWithTextAdjustStyle} />
30-
{formatLargeNumber(item.cacheWrites || 0)}
31-
<i className="codicon codicon-arrow-right" style={metadataIconWithTextAdjustStyle} />
32-
{formatLargeNumber(item.cacheReads || 0)}
19+
{!!(item.cacheReads || item.cacheWrites) && (
20+
<span className="flex items-center" data-testid="cache-compact">
21+
<i className="mr-1 codicon codicon-cloud-upload text-sm! text-vscode-descriptionForeground" />
22+
<span className="inline-block mr-1">{formatLargeNumber(item.cacheWrites || 0)}</span>
23+
<i className="mr-1 codicon codicon-cloud-download text-sm! text-vscode-descriptionForeground" />
24+
<span>{formatLargeNumber(item.cacheReads || 0)}</span>
3325
</span>
3426
)}
3527

3628
{/* Full Tokens */}
37-
{(item.tokensIn || item.tokensOut) && (
38-
<>
29+
{!!(item.tokensIn || item.tokensOut) && (
30+
<span className="flex items-center gap-1">
3931
<span data-testid="tokens-in-footer-compact">{formatLargeNumber(item.tokensIn || 0)}</span>
4032
<span data-testid="tokens-out-footer-compact">{formatLargeNumber(item.tokensOut || 0)}</span>
41-
</>
33+
</span>
4234
)}
4335

4436
{/* Full Cost */}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { render, screen, fireEvent } from "@testing-library/react"
2+
import { BatchDeleteTaskDialog } from "../BatchDeleteTaskDialog"
3+
import { vscode } from "@/utils/vscode"
4+
5+
jest.mock("@/utils/vscode")
6+
jest.mock("@/i18n/TranslationContext", () => ({
7+
useAppTranslation: () => ({
8+
t: (key: string, options?: Record<string, any>) => {
9+
const translations: Record<string, string> = {
10+
"history:deleteTasks": "Delete Tasks",
11+
"history:confirmDeleteTasks": `Are you sure you want to delete ${options?.count || 0} tasks?`,
12+
"history:deleteTasksWarning": "This action cannot be undone.",
13+
"history:cancel": "Cancel",
14+
"history:deleteItems": `Delete ${options?.count || 0} items`,
15+
}
16+
return translations[key] || key
17+
},
18+
}),
19+
}))
20+
21+
describe("BatchDeleteTaskDialog", () => {
22+
const mockTaskIds = ["task-1", "task-2", "task-3"]
23+
const mockOnOpenChange = jest.fn()
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks()
27+
})
28+
29+
it("renders dialog with correct content", () => {
30+
render(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
31+
32+
expect(screen.getByText("Delete Tasks")).toBeInTheDocument()
33+
expect(screen.getByText("Are you sure you want to delete 3 tasks?")).toBeInTheDocument()
34+
expect(screen.getByText("This action cannot be undone.")).toBeInTheDocument()
35+
expect(screen.getByText("Cancel")).toBeInTheDocument()
36+
expect(screen.getByText("Delete 3 items")).toBeInTheDocument()
37+
})
38+
39+
it("calls vscode.postMessage when delete is confirmed", () => {
40+
render(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
41+
42+
const deleteButton = screen.getByText("Delete 3 items")
43+
fireEvent.click(deleteButton)
44+
45+
expect(vscode.postMessage).toHaveBeenCalledWith({
46+
type: "deleteMultipleTasksWithIds",
47+
ids: mockTaskIds,
48+
})
49+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
50+
})
51+
52+
it("calls onOpenChange when cancel is clicked", () => {
53+
render(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
54+
55+
const cancelButton = screen.getByText("Cancel")
56+
fireEvent.click(cancelButton)
57+
58+
expect(vscode.postMessage).not.toHaveBeenCalled()
59+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
60+
})
61+
62+
it("does not call vscode.postMessage when taskIds is empty", () => {
63+
render(<BatchDeleteTaskDialog taskIds={[]} open={true} onOpenChange={mockOnOpenChange} />)
64+
65+
const deleteButton = screen.getByText("Delete 0 items")
66+
fireEvent.click(deleteButton)
67+
68+
expect(vscode.postMessage).not.toHaveBeenCalled()
69+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
70+
})
71+
72+
it("renders with correct task count in messages", () => {
73+
const singleTaskId = ["task-1"]
74+
render(<BatchDeleteTaskDialog taskIds={singleTaskId} open={true} onOpenChange={mockOnOpenChange} />)
75+
76+
expect(screen.getByText("Are you sure you want to delete 1 tasks?")).toBeInTheDocument()
77+
expect(screen.getByText("Delete 1 items")).toBeInTheDocument()
78+
})
79+
80+
it("renders trash icon in delete button", () => {
81+
render(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
82+
83+
const deleteButton = screen.getByText("Delete 3 items")
84+
const trashIcon = deleteButton.querySelector(".codicon-trash")
85+
expect(trashIcon).toBeInTheDocument()
86+
})
87+
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { render, screen, fireEvent } from "@testing-library/react"
2+
import { CopyButton } from "../CopyButton"
3+
import { useClipboard } from "@/components/ui/hooks"
4+
5+
jest.mock("@/components/ui/hooks")
6+
jest.mock("@src/i18n/TranslationContext", () => ({
7+
useAppTranslation: () => ({
8+
t: (key: string) => key,
9+
}),
10+
}))
11+
12+
describe("CopyButton", () => {
13+
const mockCopy = jest.fn()
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks()
17+
;(useClipboard as jest.Mock).mockReturnValue({
18+
isCopied: false,
19+
copy: mockCopy,
20+
})
21+
})
22+
23+
it("copies task content when clicked", () => {
24+
render(<CopyButton itemTask="Test task content" />)
25+
26+
const copyButton = screen.getByRole("button")
27+
fireEvent.click(copyButton)
28+
29+
expect(mockCopy).toHaveBeenCalledWith("Test task content")
30+
})
31+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { render, screen, fireEvent } from "@testing-library/react"
2+
import { DeleteButton } from "../DeleteButton"
3+
4+
jest.mock("@src/i18n/TranslationContext", () => ({
5+
useAppTranslation: () => ({
6+
t: (key: string) => key,
7+
}),
8+
}))
9+
10+
describe("DeleteButton", () => {
11+
it("calls onDelete when clicked", () => {
12+
const onDelete = jest.fn()
13+
render(<DeleteButton itemId="test-id" onDelete={onDelete} />)
14+
15+
const deleteButton = screen.getByRole("button")
16+
fireEvent.click(deleteButton)
17+
18+
expect(onDelete).toHaveBeenCalledWith("test-id")
19+
})
20+
})
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { render, screen, fireEvent } from "@testing-library/react"
2+
import { DeleteTaskDialog } from "../DeleteTaskDialog"
3+
import { vscode } from "@/utils/vscode"
4+
5+
jest.mock("@/utils/vscode")
6+
jest.mock("@/i18n/TranslationContext", () => ({
7+
useAppTranslation: () => ({
8+
t: (key: string) => {
9+
const translations: Record<string, string> = {
10+
"history:deleteTask": "Delete Task",
11+
"history:deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.",
12+
"history:cancel": "Cancel",
13+
"history:delete": "Delete",
14+
}
15+
return translations[key] || key
16+
},
17+
}),
18+
}))
19+
20+
jest.mock("react-use", () => ({
21+
useKeyPress: jest.fn(),
22+
}))
23+
24+
import { useKeyPress } from "react-use"
25+
26+
const mockUseKeyPress = useKeyPress as jest.MockedFunction<typeof useKeyPress>
27+
28+
describe("DeleteTaskDialog", () => {
29+
const mockTaskId = "test-task-id"
30+
const mockOnOpenChange = jest.fn()
31+
32+
beforeEach(() => {
33+
jest.clearAllMocks()
34+
mockUseKeyPress.mockReturnValue([false, null])
35+
})
36+
37+
it("renders dialog with correct content", () => {
38+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
39+
40+
expect(screen.getByText("Delete Task")).toBeInTheDocument()
41+
expect(
42+
screen.getByText("Are you sure you want to delete this task? This action cannot be undone."),
43+
).toBeInTheDocument()
44+
expect(screen.getByText("Cancel")).toBeInTheDocument()
45+
expect(screen.getByText("Delete")).toBeInTheDocument()
46+
})
47+
48+
it("calls vscode.postMessage when delete is confirmed", () => {
49+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
50+
51+
const deleteButton = screen.getByText("Delete")
52+
fireEvent.click(deleteButton)
53+
54+
expect(vscode.postMessage).toHaveBeenCalledWith({
55+
type: "deleteTaskWithId",
56+
text: mockTaskId,
57+
})
58+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
59+
})
60+
61+
it("calls onOpenChange when cancel is clicked", () => {
62+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
63+
64+
const cancelButton = screen.getByText("Cancel")
65+
fireEvent.click(cancelButton)
66+
67+
expect(vscode.postMessage).not.toHaveBeenCalled()
68+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
69+
})
70+
71+
it("does not call vscode.postMessage when taskId is empty", () => {
72+
render(<DeleteTaskDialog taskId="" open={true} onOpenChange={mockOnOpenChange} />)
73+
74+
const deleteButton = screen.getByText("Delete")
75+
fireEvent.click(deleteButton)
76+
77+
expect(vscode.postMessage).not.toHaveBeenCalled()
78+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
79+
})
80+
81+
it("handles Enter key press to delete task", () => {
82+
// Mock Enter key being pressed
83+
mockUseKeyPress.mockReturnValue([true, null])
84+
85+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
86+
87+
expect(vscode.postMessage).toHaveBeenCalledWith({
88+
type: "deleteTaskWithId",
89+
text: mockTaskId,
90+
})
91+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
92+
})
93+
94+
it("does not delete on Enter key press when taskId is empty", () => {
95+
// Mock Enter key being pressed
96+
mockUseKeyPress.mockReturnValue([true, null])
97+
98+
render(<DeleteTaskDialog taskId="" open={true} onOpenChange={mockOnOpenChange} />)
99+
100+
expect(vscode.postMessage).not.toHaveBeenCalled()
101+
expect(mockOnOpenChange).not.toHaveBeenCalled()
102+
})
103+
104+
it("calls onOpenChange on escape key", () => {
105+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
106+
107+
// Simulate escape key press on the dialog content
108+
const dialogContent = screen.getByRole("alertdialog")
109+
fireEvent.keyDown(dialogContent, { key: "Escape" })
110+
111+
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
112+
})
113+
114+
it("has correct button variants", () => {
115+
render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
116+
117+
const cancelButton = screen.getByText("Cancel")
118+
const deleteButton = screen.getByText("Delete")
119+
120+
// These should have the correct styling classes based on the component
121+
expect(cancelButton).toBeInTheDocument()
122+
expect(deleteButton).toBeInTheDocument()
123+
})
124+
125+
it("handles multiple Enter key presses correctly", () => {
126+
// First render with Enter not pressed
127+
const { rerender } = render(
128+
<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />,
129+
)
130+
131+
expect(vscode.postMessage).not.toHaveBeenCalled()
132+
133+
// Then simulate Enter key press
134+
mockUseKeyPress.mockReturnValue([true, null])
135+
rerender(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
136+
137+
expect(vscode.postMessage).toHaveBeenCalledTimes(1)
138+
expect(vscode.postMessage).toHaveBeenCalledWith({
139+
type: "deleteTaskWithId",
140+
text: mockTaskId,
141+
})
142+
})
143+
})

0 commit comments

Comments
 (0)