fix: resolve test failures in image-only message tests

- Fixed JavaScript hoisting errors by reordering function definitions
- Resolved infinite render loops in useDeepCompareEffect mock
- Added missing mocks for useSize and StandardTooltip
- Updated test scenarios to properly simulate AI busy/available states
- Fixed all ESLint warnings in test files
- All 6 image-only message tests now pass
This commit is contained in:
Roo Code 2025-07-30 02:45:40 +00:00
parent 7786171a68
commit ac2031dc53
3 changed files with 653 additions and 744 deletions

View file

@ -250,179 +250,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "playTts", text })
}
useDeepCompareEffect(() => {
// if last message is an ask, show user ask UI
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
// basically as long as a task is active, the conversation history will be persisted
if (lastMessage) {
switch (lastMessage.type) {
case "ask":
// Reset user response flag when a new ask arrives to allow auto-approval
userRespondedRef.current = false
const isPartial = lastMessage.partial === true
switch (lastMessage.ask) {
case "api_req_failed":
playSound("progress_loop")
setSendingDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
setPrimaryButtonText(t("chat:retry.title"))
setSecondaryButtonText(t("chat:startNewTask.title"))
break
case "mistake_limit_reached":
playSound("progress_loop")
setSendingDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedAnyways.title"))
setSecondaryButtonText(t("chat:startNewTask.title"))
break
case "followup":
if (!isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("followup")
// setting enable buttons to `false` would trigger a focus grab when
// the text area is enabled which is undesirable.
// We have no buttons for this tool, so no problem having them "enabled"
// to workaround this issue. See #1358.
setEnableButtons(true)
setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined)
break
case "tool":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
switch (tool.tool) {
case "editedExistingFile":
case "appliedDiff":
case "newFileCreated":
case "insertContent":
setPrimaryButtonText(t("chat:save.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "finishTask":
setPrimaryButtonText(t("chat:completeSubtaskAndReturn"))
setSecondaryButtonText(undefined)
break
case "readFile":
if (tool.batchFiles && Array.isArray(tool.batchFiles)) {
setPrimaryButtonText(t("chat:read-batch.approve.title"))
setSecondaryButtonText(t("chat:read-batch.deny.title"))
} else {
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
}
break
default:
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
}
break
case "browser_action_launch":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("browser_action_launch")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "command":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("command")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:runCommand.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "command_output":
setSendingDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
setSecondaryButtonText(t("chat:killCommand.title"))
break
case "use_mcp_server":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
if (!isPartial) {
playSound("celebration")
}
setSendingDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
break
case "resume_task":
setSendingDisabled(false)
setClineAsk("resume_task")
setEnableButtons(true)
setPrimaryButtonText(t("chat:resumeTask.title"))
setSecondaryButtonText(t("chat:terminate.title"))
setDidClickCancel(false) // special case where we reset the cancel button state
break
case "resume_completed_task":
setSendingDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
setDidClickCancel(false)
break
}
break
case "say":
// Don't want to reset since there could be a "say" after
// an "ask" while ask is waiting for response.
switch (lastMessage.say) {
case "api_req_retry_delayed":
setSendingDisabled(true)
break
case "api_req_started":
if (secondLastMessage?.ask === "command_output") {
setSendingDisabled(true)
setSelectedImages([])
setClineAsk(undefined)
setEnableButtons(false)
}
break
case "api_req_finished":
case "error":
case "text":
case "browser_action":
case "browser_action_result":
case "command_output":
case "mcp_server_request_started":
case "mcp_server_response":
case "completion_result":
break
}
break
}
}
}, [lastMessage, secondLastMessage])
useEffect(() => {
if (messages.length === 0) {
setSendingDisabled(false)
@ -1181,6 +1008,179 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
],
)
useDeepCompareEffect(() => {
// if last message is an ask, show user ask UI
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
// basically as long as a task is active, the conversation history will be persisted
if (lastMessage) {
switch (lastMessage.type) {
case "ask":
// Reset user response flag when a new ask arrives to allow auto-approval
userRespondedRef.current = false
const isPartial = lastMessage.partial === true
switch (lastMessage.ask) {
case "api_req_failed":
playSound("progress_loop")
setSendingDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
setPrimaryButtonText(t("chat:retry.title"))
setSecondaryButtonText(t("chat:startNewTask.title"))
break
case "mistake_limit_reached":
playSound("progress_loop")
setSendingDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedAnyways.title"))
setSecondaryButtonText(t("chat:startNewTask.title"))
break
case "followup":
if (!isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("followup")
// setting enable buttons to `false` would trigger a focus grab when
// the text area is enabled which is undesirable.
// We have no buttons for this tool, so no problem having them "enabled"
// to workaround this issue. See #1358.
setEnableButtons(true)
setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined)
break
case "tool":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
switch (tool.tool) {
case "editedExistingFile":
case "appliedDiff":
case "newFileCreated":
case "insertContent":
setPrimaryButtonText(t("chat:save.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "finishTask":
setPrimaryButtonText(t("chat:completeSubtaskAndReturn"))
setSecondaryButtonText(undefined)
break
case "readFile":
if (tool.batchFiles && Array.isArray(tool.batchFiles)) {
setPrimaryButtonText(t("chat:read-batch.approve.title"))
setSecondaryButtonText(t("chat:read-batch.deny.title"))
} else {
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
}
break
default:
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
}
break
case "browser_action_launch":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("browser_action_launch")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "command":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("command")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:runCommand.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "command_output":
setSendingDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
setSecondaryButtonText(t("chat:killCommand.title"))
break
case "use_mcp_server":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
}
setSendingDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
if (!isPartial) {
playSound("celebration")
}
setSendingDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
break
case "resume_task":
setSendingDisabled(false)
setClineAsk("resume_task")
setEnableButtons(true)
setPrimaryButtonText(t("chat:resumeTask.title"))
setSecondaryButtonText(t("chat:terminate.title"))
setDidClickCancel(false) // special case where we reset the cancel button state
break
case "resume_completed_task":
setSendingDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
setPrimaryButtonText(t("chat:startNewTask.title"))
setSecondaryButtonText(undefined)
setDidClickCancel(false)
break
}
break
case "say":
// Don't want to reset since there could be a "say" after
// an "ask" while ask is waiting for response.
switch (lastMessage.say) {
case "api_req_retry_delayed":
setSendingDisabled(true)
break
case "api_req_started":
if (secondLastMessage?.ask === "command_output") {
setSendingDisabled(true)
setSelectedImages([])
setClineAsk(undefined)
setEnableButtons(false)
}
break
case "api_req_finished":
case "error":
case "text":
case "browser_action":
case "browser_action_result":
case "command_output":
case "mcp_server_request_started":
case "mcp_server_response":
case "completion_result":
break
}
break
}
}
}, [lastMessage, secondLastMessage])
useEffect(() => {
// This ensures the first message is not read, future user messages are
// labeled as `user_feedback`.

View file

@ -36,6 +36,29 @@ vi.mock("@src/utils/vscode", () => ({
},
}))
// Mock useSelectedModel to return a model that supports images
vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({
useSelectedModel: vi.fn(() => ({
info: {
supportsImages: true,
maxTokens: 8192,
contextWindow: 200_000,
},
})),
}))
// Mock the API configuration and related hooks
vi.mock("@src/shared/api", () => ({
getModelMaxOutputTokens: vi.fn(() => 8192),
}))
// Mock TaskHeader to avoid API configuration issues
vi.mock("../TaskHeader", () => ({
default: function MockTaskHeader() {
return <div data-testid="task-header">Task Header</div>
},
}))
// Mock use-sound hook
const mockPlayFunction = vi.fn()
vi.mock("use-sound", () => ({
@ -68,7 +91,7 @@ vi.mock("../../common/VersionIndicator", () => ({
vi.mock("../Announcement", () => ({
default: function MockAnnouncement({ hideAnnouncement }: { hideAnnouncement: () => void }) {
const React = require("react")
const React = require("react") // eslint-disable-line @typescript-eslint/no-require-imports
return React.createElement(
"div",
{ "data-testid": "announcement-modal" },
@ -94,7 +117,6 @@ vi.mock("../QueuedMessages", () => ({
default: function MockQueuedMessages({
queue = [],
onRemove,
onUpdate,
}: {
queue?: Array<{ id: string; text: string; images: string[] }>
onRemove?: (index: number) => void
@ -178,7 +200,7 @@ const mockFocus = vi.fn()
// Create a simple mock that can test the core functionality
vi.mock("../ChatTextArea", () => {
const mockReact = require("react")
const mockReact = require("react") // eslint-disable-line @typescript-eslint/no-require-imports
return {
default: mockReact.forwardRef(function MockChatTextArea(
@ -190,6 +212,9 @@ vi.mock("../ChatTextArea", () => {
focus: mockFocus,
}))
// Use the selectedImages from props directly
const selectedImages = props.selectedImages || []
return (
<div data-testid="chat-textarea">
<input
@ -207,7 +232,17 @@ vi.mock("../ChatTextArea", () => {
disabled={props.sendingDisabled}>
Send Image Only
</button>
<div data-testid="selected-images-count">{props.selectedImages?.length || 0}</div>
<button
onClick={() => {
// Simulate adding images for testing
const newImages = ["data:image/png;base64,test1", "data:image/png;base64,test2"]
props.setSelectedImages?.(newImages)
}}
data-testid="add-images-button"
disabled={props.shouldDisableImages}>
Add Images
</button>
<div data-testid="selected-images-count">{selectedImages.length}</div>
</div>
)
}),