tests, optimizations, refactors

This commit is contained in:
Will Li 2025-07-14 14:52:06 -07:00
parent bccffac992
commit caee1dc87f
9 changed files with 634 additions and 367 deletions

View file

@ -0,0 +1,449 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { Task } from "../../task/Task"
import { ClineProvider } from "../../webview/ClineProvider"
import { checkpointSave, checkpointRestore, checkpointDiff, getCheckpointService } from "../index"
import * as vscode from "vscode"
// Mock vscode
vi.mock("vscode", () => ({
window: {
showErrorMessage: vi.fn(),
createTextEditorDecorationType: vi.fn(() => ({})),
showInformationMessage: vi.fn(),
},
Uri: {
file: vi.fn((path: string) => ({ fsPath: path })),
parse: vi.fn((uri: string) => ({ with: vi.fn(() => ({})) })),
},
commands: {
executeCommand: vi.fn(),
},
}))
// Mock other dependencies
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureCheckpointCreated: vi.fn(),
captureCheckpointRestored: vi.fn(),
captureCheckpointDiffed: vi.fn(),
},
},
}))
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/test/workspace"),
}))
vi.mock("../../../services/checkpoints")
describe("Checkpoint functionality", () => {
let mockProvider: any
let mockTask: any
let mockCheckpointService: any
beforeEach(async () => {
// Create mock checkpoint service
mockCheckpointService = {
isInitialized: true,
saveCheckpoint: vi.fn().mockResolvedValue({ commit: "test-commit-hash" }),
restoreCheckpoint: vi.fn().mockResolvedValue(undefined),
getDiff: vi.fn().mockResolvedValue([]),
on: vi.fn(),
initShadowGit: vi.fn().mockResolvedValue(undefined),
}
// Create mock provider
mockProvider = {
context: {
globalStorageUri: { fsPath: "/test/storage" },
},
log: vi.fn(),
postMessageToWebview: vi.fn(),
postStateToWebview: vi.fn(),
cancelTask: vi.fn(),
}
// Create mock task
mockTask = {
taskId: "test-task-id",
enableCheckpoints: true,
checkpointService: mockCheckpointService,
checkpointServiceInitializing: false,
providerRef: {
deref: () => mockProvider,
},
clineMessages: [],
apiConversationHistory: [],
pendingUserMessageCheckpoint: undefined,
say: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn(),
overwriteApiConversationHistory: vi.fn(),
combineMessages: vi.fn().mockReturnValue([]),
}
// Update the mock to return our mockCheckpointService
const checkpointsModule = await import("../../../services/checkpoints")
vi.mocked(checkpointsModule.RepoPerTaskCheckpointService.create).mockReturnValue(mockCheckpointService)
})
afterEach(() => {
vi.clearAllMocks()
})
describe("checkpointSave", () => {
it("should wait for checkpoint service initialization before saving", async () => {
// Set up task with uninitialized service
mockCheckpointService.isInitialized = false
mockTask.checkpointService = mockCheckpointService
// Simulate service initialization after a delay
setTimeout(() => {
mockCheckpointService.isInitialized = true
}, 100)
// Call checkpointSave
const savePromise = checkpointSave(mockTask, true)
// Wait for the save to complete
const result = await savePromise
// saveCheckpoint should have been called
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true },
)
// Result should contain the commit hash
expect(result).toEqual({ commit: "test-commit-hash" })
// Task should still have checkpoints enabled
expect(mockTask.enableCheckpoints).toBe(true)
})
it("should handle timeout when service doesn't initialize", async () => {
// Service never initializes
mockCheckpointService.isInitialized = false
// Call checkpointSave with a task that has no checkpoint service
const taskWithNoService = {
...mockTask,
checkpointService: undefined,
enableCheckpoints: false,
}
const result = await checkpointSave(taskWithNoService, true)
// Result should be undefined
expect(result).toBeUndefined()
// saveCheckpoint should not have been called
expect(mockCheckpointService.saveCheckpoint).not.toHaveBeenCalled()
})
it("should preserve checkpoint data through message deletion flow", async () => {
// Initialize service
mockCheckpointService.isInitialized = true
mockTask.checkpointService = mockCheckpointService
// Simulate saving checkpoint before user message
const checkpointResult = await checkpointSave(mockTask, true)
expect(checkpointResult).toEqual({ commit: "test-commit-hash" })
// Simulate setting pendingUserMessageCheckpoint
if (checkpointResult && "commit" in checkpointResult) {
mockTask.pendingUserMessageCheckpoint = {
hash: checkpointResult.commit,
timestamp: Date.now(),
type: "user_message",
}
}
// Verify checkpoint data is preserved
expect(mockTask.pendingUserMessageCheckpoint).toBeDefined()
expect(mockTask.pendingUserMessageCheckpoint.hash).toBe("test-commit-hash")
// Simulate message deletion and reinitialization
mockTask.clineMessages = []
mockTask.checkpointService = mockCheckpointService // Keep service available
mockTask.checkpointServiceInitializing = false
// Save checkpoint again after deletion
const newCheckpointResult = await checkpointSave(mockTask, true)
// Should still work after reinitialization
expect(newCheckpointResult).toEqual({ commit: "test-commit-hash" })
expect(mockTask.enableCheckpoints).toBe(true)
})
it("should prevent duplicate checkpoint operations for the same task", async () => {
// Start two checkpoint saves simultaneously
const promise1 = checkpointSave(mockTask)
const promise2 = checkpointSave(mockTask)
// Wait for both promises
const [result1, result2] = await Promise.all([promise1, promise2])
// Both should return the same result
expect(result1).toEqual(result2)
// saveCheckpoint should only be called once due to deduplication
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledTimes(1)
})
it("should handle errors gracefully and disable checkpoints", async () => {
mockCheckpointService.saveCheckpoint.mockRejectedValue(new Error("Save failed"))
const result = await checkpointSave(mockTask)
expect(result).toBeUndefined()
expect(mockTask.enableCheckpoints).toBe(false)
})
})
describe("checkpointRestore", () => {
beforeEach(() => {
mockTask.clineMessages = [
{ ts: 1, say: "user", text: "Message 1" },
{ ts: 2, say: "assistant", text: "Message 2" },
{ ts: 3, say: "user", text: "Message 3" },
]
mockTask.apiConversationHistory = [
{ ts: 1, role: "user", content: [{ type: "text", text: "Message 1" }] },
{ ts: 2, role: "assistant", content: [{ type: "text", text: "Message 2" }] },
{ ts: 3, role: "user", content: [{ type: "text", text: "Message 3" }] },
]
})
it("should restore checkpoint for delete operation", async () => {
await checkpointRestore(mockTask, {
ts: 2,
commitHash: "abc123",
mode: "restore",
operation: "delete",
})
expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith("abc123")
expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
{ ts: 1, role: "user", content: [{ type: "text", text: "Message 1" }] },
])
expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([{ ts: 1, say: "user", text: "Message 1" }])
expect(mockProvider.cancelTask).toHaveBeenCalled()
})
it("should restore checkpoint for edit operation", async () => {
await checkpointRestore(mockTask, {
ts: 2,
commitHash: "abc123",
mode: "restore",
operation: "edit",
})
expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith("abc123")
expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
{ ts: 1, role: "user", content: [{ type: "text", text: "Message 1" }] },
])
// For edit operation, should include the message being edited
expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([
{ ts: 1, say: "user", text: "Message 1" },
{ ts: 2, say: "assistant", text: "Message 2" },
])
expect(mockProvider.cancelTask).toHaveBeenCalled()
})
it("should handle preview mode without modifying messages", async () => {
await checkpointRestore(mockTask, {
ts: 2,
commitHash: "abc123",
mode: "preview",
})
expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith("abc123")
expect(mockTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
expect(mockTask.overwriteClineMessages).not.toHaveBeenCalled()
expect(mockProvider.cancelTask).toHaveBeenCalled()
})
it("should handle missing message gracefully", async () => {
await checkpointRestore(mockTask, {
ts: 999, // Non-existent timestamp
commitHash: "abc123",
mode: "restore",
})
expect(mockCheckpointService.restoreCheckpoint).not.toHaveBeenCalled()
})
it("should disable checkpoints on error", async () => {
mockCheckpointService.restoreCheckpoint.mockRejectedValue(new Error("Restore failed"))
await checkpointRestore(mockTask, {
ts: 2,
commitHash: "abc123",
mode: "restore",
})
expect(mockTask.enableCheckpoints).toBe(false)
expect(mockProvider.log).toHaveBeenCalledWith("[checkpointRestore] disabling checkpoints for this task")
})
})
describe("checkpointDiff", () => {
beforeEach(() => {
mockTask.clineMessages = [
{ ts: 1, say: "user", text: "Message 1" },
{ ts: 2, say: "checkpoint_saved", text: "commit1" },
{ ts: 3, say: "user", text: "Message 2" },
{ ts: 4, say: "checkpoint_saved", text: "commit2" },
]
})
it("should show diff for full mode", async () => {
const mockChanges = [
{
paths: { absolute: "/test/file.ts", relative: "file.ts" },
content: { before: "old content", after: "new content" },
},
]
mockCheckpointService.getDiff.mockResolvedValue(mockChanges)
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
})
expect(mockCheckpointService.getDiff).toHaveBeenCalledWith({
from: undefined,
to: "commit2",
})
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
"vscode.changes",
"Changes since task started",
expect.any(Array),
)
})
it("should show diff for checkpoint mode with previous commit", async () => {
const mockChanges = [
{
paths: { absolute: "/test/file.ts", relative: "file.ts" },
content: { before: "old content", after: "new content" },
},
]
mockCheckpointService.getDiff.mockResolvedValue(mockChanges)
await checkpointDiff(mockTask, {
ts: 4,
previousCommitHash: "commit1",
commitHash: "commit2",
mode: "checkpoint",
})
expect(mockCheckpointService.getDiff).toHaveBeenCalledWith({
from: "commit1",
to: "commit2",
})
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
"vscode.changes",
"Changes since previous checkpoint",
expect.any(Array),
)
})
it("should find previous checkpoint automatically in checkpoint mode", async () => {
const mockChanges = [
{
paths: { absolute: "/test/file.ts", relative: "file.ts" },
content: { before: "old content", after: "new content" },
},
]
mockCheckpointService.getDiff.mockResolvedValue(mockChanges)
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "checkpoint",
})
expect(mockCheckpointService.getDiff).toHaveBeenCalledWith({
from: "commit1", // Should find the previous checkpoint
to: "commit2",
})
})
it("should show information message when no changes found", async () => {
mockCheckpointService.getDiff.mockResolvedValue([])
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
})
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("No changes found.")
expect(vscode.commands.executeCommand).not.toHaveBeenCalled()
})
it("should disable checkpoints on error", async () => {
mockCheckpointService.getDiff.mockRejectedValue(new Error("Diff failed"))
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
})
expect(mockTask.enableCheckpoints).toBe(false)
expect(mockProvider.log).toHaveBeenCalledWith("[checkpointDiff] disabling checkpoints for this task")
})
})
describe("getCheckpointService", () => {
it("should return existing service if available", () => {
const service = getCheckpointService(mockTask)
expect(service).toBe(mockCheckpointService)
})
it("should return undefined if checkpoints are disabled", () => {
mockTask.enableCheckpoints = false
const service = getCheckpointService(mockTask)
expect(service).toBeUndefined()
})
it("should return undefined if service is still initializing", () => {
mockTask.checkpointService = undefined
mockTask.checkpointServiceInitializing = true
const service = getCheckpointService(mockTask)
expect(service).toBeUndefined()
})
it("should create new service if none exists", async () => {
mockTask.checkpointService = undefined
mockTask.checkpointServiceInitializing = false
const service = getCheckpointService(mockTask)
const checkpointsModule = await import("../../../services/checkpoints")
expect(vi.mocked(checkpointsModule.RepoPerTaskCheckpointService.create)).toHaveBeenCalledWith({
taskId: "test-task-id",
workspaceDir: "/test/workspace",
shadowDir: "/test/storage",
log: expect.any(Function),
})
})
it("should disable checkpoints if workspace path is not found", async () => {
const pathModule = await import("../../../utils/path")
vi.mocked(pathModule.getWorkspacePath).mockReturnValue(null as any)
mockTask.checkpointService = undefined
mockTask.checkpointServiceInitializing = false
const service = getCheckpointService(mockTask)
expect(service).toBeUndefined()
expect(mockTask.enableCheckpoints).toBe(false)
})
})
})

View file

@ -1,168 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { Task } from "../../task/Task"
import { ClineProvider } from "../../webview/ClineProvider"
import { checkpointSave } from "../index"
import * as vscode from "vscode"
// Mock vscode
vi.mock("vscode", () => ({
window: {
showErrorMessage: vi.fn(),
createTextEditorDecorationType: vi.fn(() => ({})),
showInformationMessage: vi.fn(),
},
Uri: {
file: vi.fn((path: string) => ({ fsPath: path })),
parse: vi.fn((uri: string) => ({ with: vi.fn(() => ({})) })),
},
commands: {
executeCommand: vi.fn(),
},
}))
// Mock other dependencies
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureCheckpointCreated: vi.fn(),
},
},
}))
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/test/workspace"),
}))
describe("Checkpoint after message deletion", () => {
let mockProvider: any
let mockTask: any
let mockCheckpointService: any
beforeEach(() => {
// Create mock checkpoint service
mockCheckpointService = {
isInitialized: true, // Set to true by default for most tests
saveCheckpoint: vi.fn().mockResolvedValue({ commit: "test-commit-hash" }),
on: vi.fn(),
initShadowGit: vi.fn().mockResolvedValue(undefined),
}
// Create mock provider
mockProvider = {
context: {
globalStorageUri: { fsPath: "/test/storage" },
},
log: vi.fn(),
postMessageToWebview: vi.fn(),
postStateToWebview: vi.fn(),
}
// Create mock task
mockTask = {
taskId: "test-task-id",
enableCheckpoints: true,
checkpointService: mockCheckpointService, // Set the service directly for most tests
checkpointServiceInitializing: false,
providerRef: {
deref: () => mockProvider,
},
clineMessages: [],
pendingUserMessageCheckpoint: undefined,
}
// Mock the RepoPerTaskCheckpointService.create to return our mock
vi.mock("../../../services/checkpoints", () => ({
RepoPerTaskCheckpointService: {
create: vi.fn(() => mockCheckpointService),
},
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it("should wait for checkpoint service initialization before saving", async () => {
// Set up task with uninitialized service
mockCheckpointService.isInitialized = false
mockTask.checkpointService = mockCheckpointService
// Simulate service initialization after a delay
setTimeout(() => {
mockCheckpointService.isInitialized = true
}, 100)
// Call checkpointSave
const savePromise = checkpointSave(mockTask, true)
// Wait for the save to complete
const result = await savePromise
// saveCheckpoint should have been called
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true },
)
// Result should contain the commit hash
expect(result).toEqual({ commit: "test-commit-hash" })
// Task should still have checkpoints enabled
expect(mockTask.enableCheckpoints).toBe(true)
})
it("should handle timeout when service doesn't initialize", async () => {
// Service never initializes
mockCheckpointService.isInitialized = false
// Call checkpointSave with a task that has no checkpoint service
const taskWithNoService = {
...mockTask,
checkpointService: undefined,
enableCheckpoints: false,
}
const result = await checkpointSave(taskWithNoService, true)
// Result should be undefined
expect(result).toBeUndefined()
// saveCheckpoint should not have been called
expect(mockCheckpointService.saveCheckpoint).not.toHaveBeenCalled()
})
it("should preserve checkpoint data through message deletion flow", async () => {
// Initialize service
mockCheckpointService.isInitialized = true
mockTask.checkpointService = mockCheckpointService
// Simulate saving checkpoint before user message
const checkpointResult = await checkpointSave(mockTask, true)
expect(checkpointResult).toEqual({ commit: "test-commit-hash" })
// Simulate setting pendingUserMessageCheckpoint
if (checkpointResult && "commit" in checkpointResult) {
mockTask.pendingUserMessageCheckpoint = {
hash: checkpointResult.commit,
timestamp: Date.now(),
type: "user_message",
}
}
// Verify checkpoint data is preserved
expect(mockTask.pendingUserMessageCheckpoint).toBeDefined()
expect(mockTask.pendingUserMessageCheckpoint.hash).toBe("test-commit-hash")
// Simulate message deletion and reinitialization
mockTask.clineMessages = []
mockTask.checkpointService = mockCheckpointService // Keep service available
mockTask.checkpointServiceInitializing = false
// Save checkpoint again after deletion
const newCheckpointResult = await checkpointSave(mockTask, true)
// Should still work after reinitialization
expect(newCheckpointResult).toEqual({ commit: "test-commit-hash" })
expect(mockTask.enableCheckpoints).toBe(true)
})
})

View file

@ -1358,12 +1358,17 @@ describe("Cline", () => {
})
it("should handle pending ask operations gracefully when task is aborted", async () => {
const [cline, task] = Task.create({
const [cline, taskPromise] = Task.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})
// Handle the task promise to prevent unhandled rejection
taskPromise.catch(() => {
// Expected error when task is aborted
})
// Start an ask operation but don't respond to it
const askPromise = cline.ask("tool", "Test question")
@ -1381,12 +1386,17 @@ describe("Cline", () => {
})
it("should not throw 'Current ask promise was ignored' error when task is aborted", async () => {
const [cline, task] = Task.create({
const [cline, taskPromise] = Task.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})
// Handle the task promise to prevent unhandled rejection
taskPromise.catch(() => {
// Expected error when task is aborted
})
// Start multiple ask operations
const askPromise1 = cline.ask("tool", "Question 1")
const askPromise2 = cline.ask("tool", "Question 2")
@ -1400,12 +1410,17 @@ describe("Cline", () => {
})
it("should resolve pending ask with messageResponse when abortTask is called", async () => {
const [cline, task] = Task.create({
const [cline, taskPromise] = Task.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})
// Handle the task promise to prevent unhandled rejection
taskPromise.catch(() => {
// Expected error when task is aborted
})
// Spy on the ask response properties
// Start an ask operation
const askPromise = cline.ask("tool", "Test question")

View file

@ -46,6 +46,12 @@ vi.mock("axios", () => ({
vi.mock("../../../utils/safeWriteJson")
vi.mock("../../../utils/storage", () => ({
getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings/path"),
getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/task/path"),
getGlobalStoragePath: vi.fn().mockResolvedValue("/test/storage/path"),
}))
vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
CallToolResultSchema: {},
ListResourcesResultSchema: {},
@ -1171,8 +1177,8 @@ describe("ClineProvider", () => {
const mockMessages = [
{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
{ ts: 2000, type: "say", say: "tool" }, // Tool message
{ ts: 3000, type: "say", say: "text", value: 4000 }, // Message to delete
{ ts: 4000, type: "say", say: "browser_action" }, // Response to delete
{ ts: 3000, type: "say", say: "text" }, // Message before delete
{ ts: 4000, type: "say", say: "browser_action" }, // Message to delete
{ ts: 5000, type: "say", say: "user_feedback" }, // Next user message
{ ts: 6000, type: "say", say: "user_feedback" }, // Final message
] as ClineMessage[]
@ -1208,22 +1214,28 @@ describe("ClineProvider", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 4000,
hasCheckpoint: false,
})
// Simulate user confirming deletion through the dialog
await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })
// Verify only messages before the deleted message were kept
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[2],
])
// Verify only API messages before the deleted message were kept
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[2],
])
// Verify initClineWithHistoryItem was called
expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" })
// initClineWithHistoryItem is only called when restoring checkpoints or aborting tasks
expect((provider as any).initClineWithHistoryItem).not.toHaveBeenCalled()
})
test("handles case when no current task exists", async () => {
@ -1253,8 +1265,8 @@ describe("ClineProvider", () => {
const mockMessages = [
{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
{ ts: 2000, type: "say", say: "tool" }, // Tool message
{ ts: 3000, type: "say", say: "text", value: 4000 }, // Message to edit
{ ts: 4000, type: "say", say: "browser_action" }, // Response to edit
{ ts: 3000, type: "say", say: "text" }, // Message before edit
{ ts: 4000, type: "say", say: "browser_action" }, // Message to edit
{ ts: 5000, type: "say", say: "user_feedback" }, // Next user message
{ ts: 6000, type: "say", say: "user_feedback" }, // Final message
] as ClineMessage[]
@ -1301,6 +1313,8 @@ describe("ClineProvider", () => {
type: "showEditMessageDialog",
messageTs: 4000,
text: "Edited message content",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming edit through the dialog
@ -1311,12 +1325,17 @@ describe("ClineProvider", () => {
})
// Verify correct messages were kept (only messages before the edited one)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[2],
])
// Verify correct API messages were kept (only messages before the edited one)
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[2],
])
// The new flow calls webviewMessageHandler recursively with askResponse
@ -2709,6 +2728,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 3000,
text: "Edited message with preserved images",
hasCheckpoint: false,
images: undefined,
})
// Simulate confirmation
@ -2718,9 +2739,9 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
text: "Edited message with preserved images",
})
// Verify messages were edited correctly - only the first message should remain
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
// Verify messages were edited correctly - messages up to the edited message should remain
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
})
test("handles editing messages with file attachments", async () => {
@ -2761,6 +2782,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 3000,
text: "Edited message with file attachment",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -2817,6 +2840,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 2000,
text: "Edited message",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -2857,6 +2882,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 2000,
text: "Edited message",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -2913,11 +2940,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 2000,
text: "Edited message 1",
hasCheckpoint: false,
images: undefined,
})
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showEditMessageDialog",
messageTs: 4000,
text: "Edited message 2",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming both edits
@ -3103,6 +3134,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 5000,
text: "Edited non-existent message",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -3143,6 +3176,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 5000,
hasCheckpoint: false,
})
// Simulate user confirming the delete
@ -3194,6 +3228,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 2000,
text: "Edited message",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -3233,6 +3269,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 2000,
hasCheckpoint: false,
})
// Simulate user confirming the delete
@ -3286,6 +3323,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: 2000,
text: largeEditedContent,
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit
@ -3328,18 +3367,23 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 3000,
hasCheckpoint: false,
})
// Simulate user confirming the delete
await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })
// Should handle large payloads without issues
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
// Should handle large payloads without issues - keeps messages before the deleted one
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
})
})
describe("Error Messaging and User Feedback", () => {
beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
})
// Note: Error messaging test removed as the implementation may not have proper error handling in place
test("provides user feedback for successful operations", async () => {
@ -3366,6 +3410,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 2000,
hasCheckpoint: false,
})
// Simulate user confirming the delete
@ -3373,7 +3418,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
// Verify successful operation completed
expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
expect(provider.initClineWithHistoryItem).toHaveBeenCalled()
// initClineWithHistoryItem is only called when restoring checkpoints or aborting tasks
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
})
@ -3439,6 +3484,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 1000,
hasCheckpoint: false,
})
// Simulate user confirming the delete
@ -3489,6 +3535,8 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
type: "showEditMessageDialog",
messageTs: futureTimestamp + 1000,
text: "Edited future message",
hasCheckpoint: false,
images: undefined,
})
// Simulate user confirming the edit

View file

@ -45,28 +45,6 @@ describe("checkpointRestoreHandler", () => {
}
})
describe("hasValidCheckpoint", () => {
it("should return true for valid checkpoint", () => {
const message = { checkpoint: { hash: "abc123" } }
expect(hasValidCheckpoint(message)).toBe(true)
})
it("should return false for missing checkpoint", () => {
const message = { text: "No checkpoint" }
expect(hasValidCheckpoint(message)).toBe(false)
})
it("should return false for invalid checkpoint structure", () => {
expect(hasValidCheckpoint({ checkpoint: "invalid" })).toBe(false)
expect(hasValidCheckpoint({ checkpoint: {} })).toBe(false)
expect(hasValidCheckpoint({ checkpoint: { hash: 123 } })).toBe(false)
})
it("should return false for empty hash", () => {
expect(hasValidCheckpoint({ checkpoint: { hash: "" } })).toBe(false)
})
})
describe("handleCheckpointRestoreOperation", () => {
describe("delete operation", () => {
it("should handle delete operation correctly", async () => {

View file

@ -10,6 +10,9 @@ vi.mock("vscode", () => ({
window: {
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: undefined,
},
}))
describe("webviewMessageHandler - checkpoint operations", () => {
@ -53,6 +56,7 @@ describe("webviewMessageHandler - checkpoint operations", () => {
historyItem: { id: "test-task-123", messages: mockCline.clineMessages },
})),
initClineWithHistoryItem: vi.fn(),
setPendingEditOperation: vi.fn(),
contextProxy: {
globalStorageUri: { fsPath: "/test/storage" },
},
@ -136,8 +140,8 @@ describe("webviewMessageHandler - checkpoint operations", () => {
operation: "edit",
})
// Verify the pending edit operation was stored
expect(mockCline.pendingEditOperation).toEqual({
// Verify the pending edit operation was stored on the provider
expect(mockProvider.setPendingEditOperation).toHaveBeenCalledWith("task-test-task-123", {
messageTs: 3,
editedContent: "Edited checkpoint message",
images: undefined,

View file

@ -502,7 +502,10 @@ describe("webviewMessageHandler - message dialog preferences", () => {
describe("deleteMessage", () => {
it("should always show dialog for delete confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
clineMessages: [],
apiConversationHistory: [],
} as any) // Mock current cline with proper structure
await webviewMessageHandler(mockClineProvider, {
type: "deleteMessage",
@ -512,13 +515,17 @@ describe("webviewMessageHandler - message dialog preferences", () => {
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 123456789,
hasCheckpoint: false,
})
})
})
describe("submitEditedMessage", () => {
it("should always show dialog for edit confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
clineMessages: [],
apiConversationHistory: [],
} as any) // Mock current cline with proper structure
await webviewMessageHandler(mockClineProvider, {
type: "submitEditedMessage",

View file

@ -868,19 +868,15 @@ describe("ChatTextArea", () => {
render(<ChatTextArea {...defaultProps} isEditMode={true} />)
// Should show edit button (codicon-edit)
const editButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-edit") !== null
},
// Should show save button in edit mode
const saveButton = screen.getByRole("button", {
name: /save/i,
})
expect(editButton).toBeInTheDocument()
expect(saveButton).toBeInTheDocument()
// Should not show send button (codicon-send)
// Should not show send button in edit mode
const sendButton = screen.queryByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-send") !== null
},
name: /send/i,
})
expect(sendButton).not.toBeInTheDocument()
})
@ -895,21 +891,17 @@ describe("ChatTextArea", () => {
render(<ChatTextArea {...defaultProps} isEditMode={false} />)
// Should show send button (codicon-send)
// Should show send button when not in edit mode
const sendButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-send") !== null
},
name: /send/i,
})
expect(sendButton).toBeInTheDocument()
// Should not show edit button (codicon-edit)
const editButton = screen.queryByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-edit") !== null
},
// Should not show save button when not in edit mode
const saveButton = screen.queryByRole("button", {
name: /save/i,
})
expect(editButton).not.toBeInTheDocument()
expect(saveButton).not.toBeInTheDocument()
})
it("should show cancel button in edit mode", () => {
@ -943,7 +935,7 @@ describe("ChatTextArea", () => {
expect(cancelButton).not.toBeInTheDocument()
})
it("should call onSend when edit button is clicked", () => {
it("should call onSend when save button is clicked", () => {
const onSend = vi.fn()
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
@ -954,13 +946,11 @@ describe("ChatTextArea", () => {
render(<ChatTextArea {...defaultProps} isEditMode={true} onSend={onSend} />)
const editButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-edit") !== null
},
const saveButton = screen.getByRole("button", {
name: /save/i,
})
fireEvent.click(editButton)
fireEvent.click(saveButton)
expect(onSend).toHaveBeenCalledTimes(1)
})
@ -981,7 +971,7 @@ describe("ChatTextArea", () => {
expect(onCancel).toHaveBeenCalledTimes(1)
})
it("should disable edit button when sendingDisabled is true", () => {
it("should disable save button when sendingDisabled is true", () => {
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
openedTabs: [],
@ -991,13 +981,11 @@ describe("ChatTextArea", () => {
render(<ChatTextArea {...defaultProps} isEditMode={true} sendingDisabled={true} />)
const editButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-edit") !== null
},
const saveButton = screen.getByRole("button", {
name: /save/i,
})
expect(editButton).toBeDisabled()
expect(saveButton).toBeDisabled()
})
it("should disable cancel button when sendingDisabled is true", () => {
@ -1014,7 +1002,7 @@ describe("ChatTextArea", () => {
expect(cancelButton).toBeDisabled()
})
it("should have correct tooltip for edit button", () => {
it("should have correct tooltip for save button", () => {
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
openedTabs: [],
@ -1024,17 +1012,16 @@ describe("ChatTextArea", () => {
render(<ChatTextArea {...defaultProps} isEditMode={true} />)
const editButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-edit") !== null
},
// Look for the save button by its aria-label
const saveButton = screen.getByRole("button", {
name: /save/i,
})
// Check that the button has the correct aria-label (which is used for tooltip)
expect(editButton).toHaveAttribute("aria-label", expect.stringMatching(/save/i))
expect(saveButton).toHaveAttribute("aria-label", expect.stringMatching(/save/i))
})
it("should position cancel button correctly relative to camera button", () => {
it("should position cancel button correctly relative to image button", () => {
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
openedTabs: [],
@ -1045,22 +1032,21 @@ describe("ChatTextArea", () => {
const { container } = render(<ChatTextArea {...defaultProps} isEditMode={true} />)
const cancelButton = screen.getByRole("button", { name: /cancel/i })
const cameraButton = screen.getByRole("button", {
name: (_, element) => {
return element.querySelector(".codicon-device-camera") !== null
},
// Look for the image button by its aria-label
const imageButton = screen.getByRole("button", {
name: /add.*images/i,
})
// Both buttons should be in the same container (bottom toolbar)
const bottomToolbar = container.querySelector(".flex.items-center.gap-0\\.5.shrink-0")
expect(bottomToolbar).toContainElement(cancelButton)
expect(bottomToolbar).toContainElement(cameraButton)
expect(bottomToolbar).toContainElement(imageButton)
// Cancel button should come before camera button in DOM order
// Cancel button should come before image button in DOM order
const buttons = bottomToolbar?.querySelectorAll("button")
const cancelIndex = Array.from(buttons || []).indexOf(cancelButton as HTMLButtonElement)
const cameraIndex = Array.from(buttons || []).indexOf(cameraButton as HTMLButtonElement)
expect(cancelIndex).toBeLessThan(cameraIndex)
const imageIndex = Array.from(buttons || []).indexOf(imageButton as HTMLButtonElement)
expect(cancelIndex).toBeLessThan(imageIndex)
})
})
})

View file

@ -13,18 +13,14 @@ vi.mock("@src/i18n/TranslationContext", () => ({
const translations: Record<string, string> = {
"common:confirmation.delete_message": "Delete Message",
"common:confirmation.edit_message": "Edit Message",
"common:confirmation.delete_warning":
"common:confirmation.delete_question_with_checkpoint":
"Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
"common:confirmation.edit_warning":
"common:confirmation.edit_question_with_checkpoint":
"Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
"common:confirmation.delete_warning_with_checkpoint":
"Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
"common:confirmation.edit_warning_with_checkpoint":
"Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
"common:confirmation.restore_checkpoint": "Do you also wish to revert code to this checkpoint?",
"common:confirmation.proceed": "Proceed",
"common:confirmation.edit_only": "Edit Only",
"common:confirmation.delete_only": "Delete Only",
"common:confirmation.restore_to_checkpoint": "Restore to Checkpoint",
"common:answers.cancel": "Cancel",
"common:confirmation.dont_show_again": "Don't show this again",
}
return translations[key] || key
},
@ -54,9 +50,9 @@ describe("CheckpointRestoreDialog", () => {
"Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
),
).toBeInTheDocument()
expect(screen.getByText("Proceed")).toBeInTheDocument()
expect(screen.getByText("Edit Only")).toBeInTheDocument()
expect(screen.getByText("Cancel")).toBeInTheDocument()
expect(screen.queryByText("Do you also wish to revert code to this checkpoint?")).not.toBeInTheDocument()
expect(screen.queryByText("Restore to Checkpoint")).not.toBeInTheDocument()
})
it("renders delete dialog without checkpoint", () => {
@ -68,9 +64,9 @@ describe("CheckpointRestoreDialog", () => {
"Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
),
).toBeInTheDocument()
expect(screen.getByText("Proceed")).toBeInTheDocument()
expect(screen.getByText("Delete Only")).toBeInTheDocument()
expect(screen.getByText("Cancel")).toBeInTheDocument()
expect(screen.queryByText("Do you also wish to revert code to this checkpoint?")).not.toBeInTheDocument()
expect(screen.queryByText("Restore to Checkpoint")).not.toBeInTheDocument()
})
it("renders edit dialog with checkpoint option", () => {
@ -82,8 +78,9 @@ describe("CheckpointRestoreDialog", () => {
"Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
),
).toBeInTheDocument()
expect(screen.getByText("Do you also wish to revert code to this checkpoint?")).toBeInTheDocument()
expect(screen.getAllByRole("checkbox")).toHaveLength(2) // restore and dont show again
expect(screen.getByText("Edit Only")).toBeInTheDocument()
expect(screen.getByText("Restore to Checkpoint")).toBeInTheDocument()
expect(screen.getByText("Cancel")).toBeInTheDocument()
})
it("renders delete dialog with checkpoint option", () => {
@ -95,8 +92,9 @@ describe("CheckpointRestoreDialog", () => {
"Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
),
).toBeInTheDocument()
expect(screen.getByText("Do you also wish to revert code to this checkpoint?")).toBeInTheDocument()
expect(screen.getAllByRole("checkbox")).toHaveLength(2) // restore and dont show again
expect(screen.getByText("Delete Only")).toBeInTheDocument()
expect(screen.getByText("Restore to Checkpoint")).toBeInTheDocument()
expect(screen.getByText("Cancel")).toBeInTheDocument()
})
})
@ -109,73 +107,36 @@ describe("CheckpointRestoreDialog", () => {
expect(onOpenChange).toHaveBeenCalledWith(false)
})
it("calls onConfirm with correct parameters when proceed is clicked without checkpoint", () => {
it("calls onConfirm with correct parameters when edit only is clicked", () => {
const onConfirm = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onConfirm={onConfirm} />)
fireEvent.click(screen.getByText("Proceed"))
expect(onConfirm).toHaveBeenCalledWith(false, false) // dontShowAgain, restoreCheckpoint
fireEvent.click(screen.getByText("Edit Only"))
expect(onConfirm).toHaveBeenCalledWith(false) // restoreCheckpoint
})
it("calls onConfirm with restoreCheckpoint=false when proceed is clicked with unchecked checkbox", () => {
it("calls onConfirm with restoreCheckpoint=false when edit only is clicked with checkpoint", () => {
const onConfirm = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onConfirm={onConfirm} hasCheckpoint={true} />)
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
expect(restoreCheckbox).not.toBeChecked()
fireEvent.click(screen.getByText("Proceed"))
expect(onConfirm).toHaveBeenCalledWith(false, false) // dontShowAgain, restoreCheckpoint
fireEvent.click(screen.getByText("Edit Only"))
expect(onConfirm).toHaveBeenCalledWith(false) // restoreCheckpoint
})
it("calls onConfirm with restoreCheckpoint=true when proceed is clicked with checked checkbox", () => {
it("calls onConfirm with restoreCheckpoint=true when restore to checkpoint is clicked", () => {
const onConfirm = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onConfirm={onConfirm} hasCheckpoint={true} />)
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
fireEvent.click(restoreCheckbox)
expect(restoreCheckbox).toBeChecked()
fireEvent.click(screen.getByText("Proceed"))
expect(onConfirm).toHaveBeenCalledWith(false, true) // dontShowAgain, restoreCheckpoint
fireEvent.click(screen.getByText("Restore to Checkpoint"))
expect(onConfirm).toHaveBeenCalledWith(true) // restoreCheckpoint
})
it("toggles restore checkpoint checkbox state when clicked", () => {
render(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} />)
it("calls onOpenChange when dialog is closed", () => {
const onOpenChange = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onOpenChange={onOpenChange} hasCheckpoint={true} />)
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
expect(restoreCheckbox).not.toBeChecked()
fireEvent.click(restoreCheckbox)
expect(restoreCheckbox).toBeChecked()
fireEvent.click(restoreCheckbox)
expect(restoreCheckbox).not.toBeChecked()
})
it("toggles dont show again checkbox state when clicked", () => {
render(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} />)
const dontShowCheckbox = screen.getByLabelText("Don't show this again")
expect(dontShowCheckbox).not.toBeChecked()
fireEvent.click(dontShowCheckbox)
expect(dontShowCheckbox).toBeChecked()
fireEvent.click(dontShowCheckbox)
expect(dontShowCheckbox).not.toBeChecked()
})
it("calls onConfirm with dontShowAgain=true when dont show again is checked", () => {
const onConfirm = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onConfirm={onConfirm} hasCheckpoint={true} />)
const dontShowCheckbox = screen.getByLabelText("Don't show this again")
fireEvent.click(dontShowCheckbox)
expect(dontShowCheckbox).toBeChecked()
fireEvent.click(screen.getByText("Proceed"))
expect(onConfirm).toHaveBeenCalledWith(true, false) // dontShowAgain, restoreCheckpoint
fireEvent.click(screen.getByText("Edit Only"))
expect(onOpenChange).toHaveBeenCalledWith(false)
})
})
@ -187,29 +148,19 @@ describe("CheckpointRestoreDialog", () => {
expect(screen.queryByText("Delete Message")).not.toBeInTheDocument()
})
it("resets checkbox states when dialog reopens", async () => {
it("maintains state when dialog stays open", async () => {
const { rerender } = render(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} open={true} />)
// Check both checkboxes
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
const dontShowCheckbox = screen.getByLabelText("Don't show this again")
// Verify initial state
expect(screen.getByText("Edit Only")).toBeInTheDocument()
expect(screen.getByText("Restore to Checkpoint")).toBeInTheDocument()
fireEvent.click(restoreCheckbox)
fireEvent.click(dontShowCheckbox)
expect(restoreCheckbox).toBeChecked()
expect(dontShowCheckbox).toBeChecked()
// Close dialog
rerender(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} open={false} />)
// Reopen dialog - useEffect should reset state when open becomes true
// Re-render with same props
rerender(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} open={true} />)
// Checkboxes should be unchecked after reopening due to useEffect
const newRestoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
const newDontShowCheckbox = screen.getByLabelText("Don't show this again")
expect(newRestoreCheckbox).not.toBeChecked()
expect(newDontShowCheckbox).not.toBeChecked()
// Should still have same buttons
expect(screen.getByText("Edit Only")).toBeInTheDocument()
expect(screen.getByText("Restore to Checkpoint")).toBeInTheDocument()
})
})
@ -218,20 +169,10 @@ describe("CheckpointRestoreDialog", () => {
render(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} />)
expect(screen.getByRole("alertdialog")).toBeInTheDocument() // AlertDialog uses alertdialog role
expect(screen.getAllByRole("checkbox")).toHaveLength(2) // restore and dont show again
expect(screen.getByRole("button", { name: "Proceed" })).toBeInTheDocument()
expect(screen.getByRole("button", { name: "Edit Only" })).toBeInTheDocument()
expect(screen.getByRole("button", { name: "Restore to Checkpoint" })).toBeInTheDocument()
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument()
})
it("checkboxes are properly labeled", () => {
render(<CheckpointRestoreDialog {...defaultProps} hasCheckpoint={true} />)
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
const dontShowCheckbox = screen.getByLabelText("Don't show this again")
expect(restoreCheckbox).toBeInTheDocument()
expect(dontShowCheckbox).toBeInTheDocument()
})
})
describe("Edge Cases", () => {
@ -244,20 +185,27 @@ describe("CheckpointRestoreDialog", () => {
expect(screen.getByText("Edit Message")).toBeInTheDocument()
})
it("handles rapid state changes", async () => {
it("handles rapid button clicks", async () => {
const onConfirm = vi.fn()
render(<CheckpointRestoreDialog {...defaultProps} onConfirm={onConfirm} hasCheckpoint={true} />)
const onOpenChange = vi.fn()
render(
<CheckpointRestoreDialog
{...defaultProps}
onConfirm={onConfirm}
onOpenChange={onOpenChange}
hasCheckpoint={true}
/>,
)
const restoreCheckbox = screen.getByLabelText("Do you also wish to revert code to this checkpoint?")
const proceedButton = screen.getByText("Proceed")
const editOnlyButton = screen.getByText("Edit Only")
// Rapidly toggle checkbox and click proceed
fireEvent.click(restoreCheckbox)
fireEvent.click(restoreCheckbox)
fireEvent.click(restoreCheckbox)
fireEvent.click(proceedButton)
// Click button once
fireEvent.click(editOnlyButton)
expect(onConfirm).toHaveBeenCalledWith(false, true) // dontShowAgain, restoreCheckpoint
// Should be called once with correct parameters
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(onConfirm).toHaveBeenCalledWith(false) // restoreCheckpoint
expect(onOpenChange).toHaveBeenCalledWith(false) // dialog should close
})
})