Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { webviewMessageHandler } from "../webviewMessageHandler"
import * as processFiles from "../../../integrations/misc/process-files"
import { FileReference, FileAttachment } from "../../../shared/FileTypes"

// Mock the process-files module
vi.mock("../../../integrations/misc/process-files", () => ({
selectFiles: vi.fn(),
convertImageReferencesToDataUrls: vi.fn(),
}))

describe("webviewMessageHandler - selectFiles", () => {
let mockProvider: any

beforeEach(() => {
mockProvider = {
postMessageToWebview: vi.fn(),
}
vi.clearAllMocks()
})

it("should convert FileReference[] to string[] for images when handling selectFiles", async () => {
// Mock data
const mockFileReferences: FileReference[] = [
{
path: "image1.png",
uri: "file:///path/to/image1.png",
size: 1024,
mimeType: "image/png",
},
{
path: "image2.jpg",
uri: "file:///path/to/image2.jpg",
size: 2048,
mimeType: "image/jpeg",
},
]

const mockFileAttachments: FileAttachment[] = [
{
path: "file1.txt",
content: "Hello world",
type: "txt",
},
]

const mockDataUrls = [
"data:image/png;base64,iVBORw0KGgoAAAANS...",
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...",
]

// Setup mocks
vi.mocked(processFiles.selectFiles).mockResolvedValue({
images: mockFileReferences,
files: mockFileAttachments,
})
vi.mocked(processFiles.convertImageReferencesToDataUrls).mockResolvedValue(mockDataUrls)

// Execute
await webviewMessageHandler(mockProvider, { type: "selectFiles" })

// Verify
expect(processFiles.selectFiles).toHaveBeenCalledOnce()
expect(processFiles.convertImageReferencesToDataUrls).toHaveBeenCalledWith(mockFileReferences)
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "selectedFiles",
images: mockDataUrls,
files: mockFileAttachments,
})
})

it("should handle empty file selection", async () => {
// Setup mocks for empty selection
vi.mocked(processFiles.selectFiles).mockResolvedValue({
images: [],
files: [],
})
vi.mocked(processFiles.convertImageReferencesToDataUrls).mockResolvedValue([])

// Execute
await webviewMessageHandler(mockProvider, { type: "selectFiles" })

// Verify
expect(processFiles.selectFiles).toHaveBeenCalledOnce()
expect(processFiles.convertImageReferencesToDataUrls).toHaveBeenCalledWith([])
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "selectedFiles",
images: [],
files: [],
})
})
})
8 changes: 7 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { experimentDefault } from "../../shared/experiments"
import { Terminal } from "../../integrations/terminal/Terminal"
import { openFile } from "../../integrations/misc/open-file"
import { openImage, saveImage } from "../../integrations/misc/image-handler"
import { selectImages } from "../../integrations/misc/process-images"
import { selectImages, selectFiles, convertImageReferencesToDataUrls } from "../../integrations/misc/process-files"
import { getTheme } from "../../integrations/theme/getTheme"
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
import { searchWorkspaceFiles } from "../../services/search/file-search"
Expand Down Expand Up @@ -418,6 +418,12 @@ export const webviewMessageHandler = async (
const images = await selectImages()
await provider.postMessageToWebview({ type: "selectedImages", images })
break
case "selectFiles":
const { images: fileImages, files } = await selectFiles()
// Convert FileReference[] to string[] (base64 data URLs)
const imageDataUrls = await convertImageReferencesToDataUrls(fileImages)
await provider.postMessageToWebview({ type: "selectedFiles", images: imageDataUrls, files })
break
case "exportCurrentTask":
const currentTaskId = provider.getCurrentCline()?.taskId
if (currentTaskId) {
Expand Down
241 changes: 241 additions & 0 deletions src/integrations/misc/__tests__/process-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import * as vscode from "vscode"
import fs from "fs/promises"
import * as path from "path"
import { selectFiles, selectImages, convertImageReferencesToDataUrls } from "../process-files"
import { getMimeType, isImageExtension } from "../../../utils/mimeTypes"

// Mock vscode
vi.mock("vscode", () => ({
window: {
showOpenDialog: vi.fn(),
showWarningMessage: vi.fn(),
},
Uri: {
file: (path: string) => ({ fsPath: path, toString: () => `file://${path}` }),
parse: (uri: string) => ({ fsPath: uri.replace("file://", "") }),
},
}))

// Mock fs
vi.mock("fs/promises", () => ({
default: {
readFile: vi.fn(),
stat: vi.fn(),
},
}))

// Mock mimeTypes utils
vi.mock("../../../utils/mimeTypes", () => ({
getMimeType: vi.fn(),
isImageExtension: vi.fn(),
}))

describe("process-files", () => {
beforeEach(() => {
vi.clearAllMocks()
})

describe("selectFiles", () => {
it("should return empty arrays when no files are selected", async () => {
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue(undefined)

const result = await selectFiles()

expect(result).toEqual({ images: [], files: [] })
})

it("should process image files correctly with references", async () => {
const mockUri = {
fsPath: "/test/image.png",
toString: () => "file:///test/image.png",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValue(true)
vi.mocked(getMimeType).mockReturnValue("image/png")

const result = await selectFiles()

expect(result.images).toHaveLength(1)
expect(result.images[0]).toEqual({
path: "image.png",
uri: "file:///test/image.png",
size: 1000,
mimeType: "image/png",
})
expect(result.files).toHaveLength(0)
})

it("should process text files correctly", async () => {
const mockUri = {
fsPath: "/test/data.json",
toString: () => "file:///test/data.json",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValue(false)
vi.mocked(fs.readFile).mockResolvedValue('{"test": "data"}')

const result = await selectFiles()

expect(result.files).toHaveLength(1)
expect(result.files[0]).toEqual({
path: "data.json",
content: '{"test": "data"}',
type: "json",
})
expect(result.images).toHaveLength(0)
})

it("should handle multiple files of different types", async () => {
const mockUris = [
{ fsPath: "/test/image.jpg", toString: () => "file:///test/image.jpg" },
{ fsPath: "/test/config.xml", toString: () => "file:///test/config.xml" },
{ fsPath: "/test/readme.md", toString: () => "file:///test/readme.md" },
]
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue(mockUris as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValueOnce(true).mockReturnValueOnce(false).mockReturnValueOnce(false)
vi.mocked(getMimeType).mockReturnValue("image/jpeg")
vi.mocked(fs.readFile).mockResolvedValueOnce("<config>test</config>").mockResolvedValueOnce("# README")

const result = await selectFiles()

expect(result.images).toHaveLength(1)
expect(result.files).toHaveLength(2)
expect(result.files[0].type).toBe("xml")
expect(result.files[1].type).toBe("md")
})

it("should reject files that are too large", async () => {
const mockUri = {
fsPath: "/test/large.txt",
toString: () => "file:///test/large.txt",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 25 * 1024 * 1024 } as any) // 25MB

const result = await selectFiles()

expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("File too large: large.txt (max 20MB)")
expect(result).toEqual({ images: [], files: [] })
})

it("should handle binary files gracefully", async () => {
const mockUri = {
fsPath: "/test/binary.exe",
toString: () => "file:///test/binary.exe",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValue(false)

const result = await selectFiles()

expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("Cannot attach binary file: binary.exe")
expect(result).toEqual({ images: [], files: [] })
})

it("should detect binary content in text files", async () => {
const mockUri = {
fsPath: "/test/fake-text.txt",
toString: () => "file:///test/fake-text.txt",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValue(false)
vi.mocked(fs.readFile).mockResolvedValue("text with \0 null byte")

const result = await selectFiles()

expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("File appears to be binary: fake-text.txt")
expect(result).toEqual({ images: [], files: [] })
})

it("should handle file read errors", async () => {
const mockUri = {
fsPath: "/test/unreadable.txt",
toString: () => "file:///test/unreadable.txt",
}
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([mockUri] as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValue(false)
vi.mocked(fs.readFile).mockRejectedValue(new Error("Permission denied"))

const result = await selectFiles()

expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("Cannot read file as text: unreadable.txt")
expect(result).toEqual({ images: [], files: [] })
})
})

describe("convertImageReferencesToDataUrls", () => {
it("should convert image references to data URLs", async () => {
const imageRefs = [
{
path: "image1.png",
uri: "file:///test/image1.png",
size: 1000,
mimeType: "image/png",
},
{
path: "image2.jpg",
uri: "file:///test/image2.jpg",
size: 2000,
mimeType: "image/jpeg",
},
]

vi.mocked(fs.readFile)
.mockResolvedValueOnce(Buffer.from("fake-png-data"))
.mockResolvedValueOnce(Buffer.from("fake-jpg-data"))

const result = await convertImageReferencesToDataUrls(imageRefs)

expect(result).toHaveLength(2)
expect(result[0]).toBe("data:image/png;base64,ZmFrZS1wbmctZGF0YQ==")
expect(result[1]).toBe("data:image/jpeg;base64,ZmFrZS1qcGctZGF0YQ==")
})

it("should handle errors when converting images", async () => {
const imageRefs = [
{
path: "missing.png",
uri: "file:///test/missing.png",
size: 1000,
mimeType: "image/png",
},
]

vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"))

const result = await convertImageReferencesToDataUrls(imageRefs)

expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("Failed to read image: missing.png")
expect(result).toEqual([])
})
})

describe("selectImages", () => {
it("should only return images from selectFiles as data URLs", async () => {
const mockUris = [
{ fsPath: "/test/image.png", toString: () => "file:///test/image.png" },
{ fsPath: "/test/data.json", toString: () => "file:///test/data.json" },
]
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue(mockUris as any)
vi.mocked(fs.stat).mockResolvedValue({ size: 1000 } as any)
vi.mocked(isImageExtension).mockReturnValueOnce(true).mockReturnValueOnce(false)
vi.mocked(getMimeType).mockReturnValue("image/png")
// First call for text file read attempt, second for image conversion
vi.mocked(fs.readFile)
.mockResolvedValueOnce('{"test": "data"}')
.mockResolvedValueOnce(Buffer.from("fake-image"))

const result = await selectImages()

expect(result).toHaveLength(1)
expect(result[0]).toBe("data:image/png;base64,ZmFrZS1pbWFnZQ==")
})
})
})
Loading