Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
186 changes: 186 additions & 0 deletions webview-ui/src/utils/__tests__/context-mentions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,81 @@ describe("insertMention", () => {
expect(result.newValue).toBe("@test ")
expect(result.mentionIndex).toBe(0)
})

it("should handle mentions with spaces correctly", () => {
const result = insertMention("Hello world", 5, "file with spaces.txt")
expect(result.newValue).toBe("Hello@file\\ with\\ spaces.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should replace text after last @ symbol with spaces in mention", () => {
const result = insertMention("Hello @wor world", 8, "file with spaces.txt")
expect(result.newValue).toBe("Hello @file\\ with\\ spaces.txt world")
expect(result.mentionIndex).toBe(6)
})

it("should handle file names with multiple consecutive spaces", () => {
const result = insertMention("Hello world", 5, "file with multiple spaces.txt")
expect(result.newValue).toBe("Hello@file\\ \\ with\\ \\ \\ multiple\\ \\ \\ \\ spaces.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should handle file names with special characters", () => {
const result = insertMention("Hello world", 5, "file-with_special#chars&.txt")
expect(result.newValue).toBe("Hello@file-with_special#chars&.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should handle file names with unicode characters", () => {
const result = insertMention("Hello world", 5, "文件名with空格and字符.txt")
expect(result.newValue).toBe("Hello@文件名with空格and字符.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should handle paths with spaces", () => {
const result = insertMention("Hello world", 5, "path with/spaces/in it/file.txt")
expect(result.newValue).toBe("Hello@path\\ with/spaces/in\\ it/file.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should handle cursor at beginning of text", () => {
const result = insertMention("Hello world", 0, "test.txt")
expect(result.newValue).toBe("@test.txt Hello world")
expect(result.mentionIndex).toBe(0)
})

it("should handle cursor at end of text", () => {
const result = insertMention("Hello world", 11, "test.txt")
expect(result.newValue).toBe("Hello [email protected] ")
expect(result.mentionIndex).toBe(11)
})

it("should handle text with multiple @ symbols", () => {
const result = insertMention("Hello @first and @second world", 19, "test.txt")
expect(result.newValue).toBe("Hello @first and @test.txt world")
expect(result.mentionIndex).toBe(17)
})

it("should handle file names already containing backslashes", () => {
const result = insertMention("Hello world", 5, "file\\with\\backslashes.txt")
// Backslashes should be preserved and spaces should be escaped
expect(result.newValue).toBe("Hello@file\\with\\backslashes.txt world")
expect(result.mentionIndex).toBe(5)
})

it("should handle very long file names", () => {
const longName = "a".repeat(100) + " " + "b".repeat(100) + ".txt"
const result = insertMention("Hello world", 5, longName)
expect(result.newValue).toBe(`Hello@${"a".repeat(100)}\\ ${"b".repeat(100)}.txt world`)
expect(result.mentionIndex).toBe(5)
})

it("should handle file names already containing escaped spaces", () => {
const result = insertMention("Hello world", 5, "file\\ with\\ spaces.txt")
// It should correctly handle the already escaped spaces without double-escaping
expect(result.newValue).toBe("Hello@file\\\\ with\\\\ spaces.txt world")
expect(result.mentionIndex).toBe(5)
})
})

describe("removeMention", () => {
Expand Down Expand Up @@ -338,3 +413,114 @@ describe("shouldShowContextMenu", () => {
expect(shouldShowContextMenu("@problems", 9)).toBe(true)
})
})

/**
* Integration tests for the complete file mention workflow
* These tests simulate the actual user flow when selecting files from the list
*
* IMPORTANT NOTE ABOUT BACKSLASH ESCAPING IN TESTS:
* ================================================
* The escaping of spaces in file paths is a multi-level process:
*
* 1. In JavaScript strings, a single backslash must be written as "\\".
* So to represent "\" in the string, we write it as "\\".
*
* 2. When we want to escape a space in a path, we use "\ " (backslash + space).
* But in JavaScript code, this must be written as "\\ ".
*
* 3. Our escaping pipeline has two phases:
* - First in convertToMentionPath: spaces -> "\ "
* - Then in insertMention: "\ " -> "\\ "
*
* 4. Due to this multi-level escaping, paths in our expect() statements
* contain four backslashes "\\\\" for each escaped space.
*
* This is NOT a bug but the expected behavior:
* - In JS, "\\\\" represents two literal backslashes "\\"
* - When the string is processed, these become the actual double escape
* that's displayed to the user in their editor
*
* Example:
* - Original path: "/docs/file with spaces.txt"
* - After convertToMentionPath: "@/docs/file\ with\ spaces.txt"
* - After insertMention: "@/docs/file\\ with\\ spaces.txt"
* - In test code expect(): "...@/docs/file\\\\ with\\\\ spaces.txt..."
*/
describe("File selection integration tests", () => {
// Import the convertToMentionPath function to use in the integration tests
const { convertToMentionPath } = require("../path-mentions")

it("should correctly handle selection of file with spaces from file list", () => {
// Step 1: User has text in editor with cursor position
const originalText = "Check out this file: "
const cursorPosition = originalText.length

// Step 2: User selects a file with spaces from the list
// The file path would be converted to a mention path
const selectedFilePath = "/Users/username/project/documents/report with spaces.pdf"
const projectRoot = "/Users/username/project"
const mentionPath = convertToMentionPath(selectedFilePath, projectRoot)

// Verify mention path has single-escaped spaces (written with double backslashes in code)
// Actual: "@/documents/report\ with\ spaces.pdf"
expect(mentionPath).toBe("@/documents/report\\ with\\ spaces.pdf")

// Step 3: The mention path is inserted at cursor position
// We remove the @ since insertMention adds it back
const result = insertMention(originalText, cursorPosition, mentionPath.substring(1))

// Step 4: Verify the final text has proper escaping and format
// Note: Double backslashes in the actual output require four backslashes in the code
// Actual output: "Check out this file: @/documents/report\\ with\\ spaces.pdf "
expect(result.newValue).toBe("Check out this file: @/documents/report\\\\ with\\\\ spaces.pdf ")
})

it("should correctly handle selection of file with special characters from file list", () => {
// Step 1: User has text in editor with cursor position
const originalText = "Document reference: "
const cursorPosition = originalText.length

// Step 2: User selects a file with special characters
const selectedFilePath = "/Users/username/project/docs/report#1 (2023) final-v2.pdf"
const projectRoot = "/Users/username/project"
const mentionPath = convertToMentionPath(selectedFilePath, projectRoot)

// Verify mention path has single-escaped spaces (written with double backslashes in code)
// Actual: "@/docs/report#1\ (2023)\ final-v2.pdf"
expect(mentionPath).toBe("@/docs/report#1\\ (2023)\\ final-v2.pdf")

// Step 3: The mention path is inserted at cursor position
// We remove the @ since insertMention adds it back
const result = insertMention(originalText, cursorPosition, mentionPath.substring(1))

// Step 4: Verify the final text has proper escaping and format
// Note: Double backslashes in the actual output require four backslashes in the code
// Actual output: "Document reference: @/docs/report#1\\ (2023)\\ final-v2.pdf "
expect(result.newValue).toBe("Document reference: @/docs/report#1\\\\ (2023)\\\\ final-v2.pdf ")
})

it("should correctly handle selection of nested file with spaces from file list", () => {
// Step 1: User has text with existing mentions
const originalText = "Compare with @/other/file.txt and "
const cursorPosition = originalText.length

// Step 2: User selects a nested file with spaces
const selectedFilePath = "/Users/username/project/nested/folder with spaces/file with spaces.txt"
const projectRoot = "/Users/username/project"
const mentionPath = convertToMentionPath(selectedFilePath, projectRoot)

// Verify mention path has single-escaped spaces (written with double backslashes in code)
// Actual: "@/nested/folder\ with\ spaces/file\ with\ spaces.txt"
expect(mentionPath).toBe("@/nested/folder\\ with\\ spaces/file\\ with\\ spaces.txt")

// Step 3: The mention path is inserted at cursor position
// We remove the @ since insertMention adds it back
const result = insertMention(originalText, cursorPosition, mentionPath.substring(1))

// Step 4: Verify the final text has proper escaping and format
// Note: Double backslashes in the actual output require four backslashes in the code
// Important: The original behavior replaces the existing text after the @ symbol
// Actual output: "Compare with @/nested/folder\\ with\\ spaces/file\\ with\\ spaces.txt "
expect(result.newValue).toBe("Compare with @/nested/folder\\\\ with\\\\ spaces/file\\\\ with\\\\ spaces.txt ")
})
})
122 changes: 122 additions & 0 deletions webview-ui/src/utils/__tests__/path-mentions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { convertToMentionPath } from "../path-mentions"

/**
* IMPORTANT NOTE ABOUT BACKSLASH ESCAPING IN TESTS:
* ================================================
* The handling of backslashes and spaces in paths is complex due to multiple levels of escaping:
*
* 1. JavaScript string escaping:
* - A single backslash in code must be written as "\\".
* - For example, "C:\\path\\file.txt" represents the path "C:\path\file.txt".
*
* 2. Path normalization:
* - Windows backslashes are converted to forward slashes.
* - "C:\\path\\file.txt" becomes "C:/path/file.txt".
*
* 3. Space escaping:
* - When a path contains spaces, we escape them with backslashes.
* - "path/file with spaces.txt" becomes "path/file\ with\ spaces.txt".
* - In code, this looks like: "path/file\\ with\\ spaces.txt".
*
* 4. Multiple escaping:
* - For paths that already contain escaped spaces, the escaping gets doubled.
* - A path with already escaped spaces like "path/file\ with\ spaces.txt"
* (which in code is "path/file\\ with\\ spaces.txt")
* gets double-escaped to "path/file\\ with\\ spaces.txt"
* (which in code is "path/file\\\\ with\\\\ spaces.txt").
*
* The tests in this file verify these complex escaping behaviors, which may appear
* confusing at first glance but are necessary to ensure proper handling of paths
* with spaces across the application.
*/
describe("path-mentions", () => {
describe("convertToMentionPath", () => {
it("should convert an absolute path to a mention path when it starts with cwd", () => {
Expand Down Expand Up @@ -41,5 +70,98 @@
"@/nested/deeply/file.txt",
)
})

it("should handle paths with spaces correctly", () => {
expect(
convertToMentionPath("C:\\Users\\user\\project\\file with spaces.txt", "C:\\Users\\user\\project"),
).toBe("@/file\\ with\\ spaces.txt")

expect(convertToMentionPath("/Users/user/project/file with spaces.txt", "/Users/user/project")).toBe(
"@/file\\ with\\ spaces.txt",
)
})

it("should handle paths with multiple consecutive spaces", () => {
expect(
convertToMentionPath("C:\\Users\\user\\project\\file with spaces.txt", "C:\\Users\\user\\project"),
).toBe("@/file\\ \\ with\\ \\ \\ spaces.txt")
})

it("should handle paths with Unicode characters", () => {
expect(
convertToMentionPath("C:\\Users\\user\\project\\文件名with空格.txt", "C:\\Users\\user\\project"),
).toBe("@/文件名with空格.txt")
})

it("should handle paths with special characters", () => {
expect(
convertToMentionPath(
"C:\\Users\\user\\project\\file-with_special#chars&.txt",
"C:\\Users\\user\\project",
),
).toBe("@/file-with_special#chars&.txt")
})

it("should handle nested paths with spaces", () => {
expect(
convertToMentionPath(
"C:\\Users\\user\\project\\folder with spaces\\file with spaces.txt",
"C:\\Users\\user\\project",
),
).toBe("@/folder\\ with\\ spaces/file\\ with\\ spaces.txt")
})

it("should handle extremely long paths", () => {
const longDirectoryName = "a".repeat(50)
const longFileName = "b".repeat(50) + " " + "c".repeat(50) + ".txt"
const fullPath = `C:\\Users\\user\\project\\${longDirectoryName}\\${longFileName}`

expect(convertToMentionPath(fullPath, "C:\\Users\\user\\project")).toBe(
`@/${longDirectoryName}/${longFileName.replace(/ /g, "\\ ")}`,

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High test

This does not escape backslash characters in the input.
)
})

it("should handle paths with already escaped spaces", () => {
// Note: The input has literal backslash-space sequences which must be written with double backslashes in JS
// Input in code: "C:\\Users\\user\\project\\file\\ with\\ spaces.txt"
// Actual string: "C:\Users\user\project\file\ with\ spaces.txt"
expect(
convertToMentionPath("C:\\Users\\user\\project\\file\\ with\\ spaces.txt", "C:\\Users\\user\\project"),
).toBe(
// Expected output has double-escaped spaces (written with four backslashes in code)
"@/file/\\ with/\\ spaces.txt",
)

// Same test with Unix path format
expect(convertToMentionPath("/Users/user/project/file\\ with\\ spaces.txt", "/Users/user/project")).toBe(
"@/file/\\ with/\\ spaces.txt",
)
})

it("should handle UNC paths", () => {
expect(convertToMentionPath("\\\\server\\share\\folder with spaces\\file.txt", "\\\\server\\share")).toBe(
"@/folder\\ with\\ spaces/file.txt",
)
})

it("should handle properly escaped backslashes", () => {
// Each \\\\ in the code represents two backslashes in the actual string
// Input in code: "C:\\Users\\user\\project\\file\\\\ with\\\\ spaces.txt"
// Actual string: "C:\Users\user\project\file\\ with\\ spaces.txt"
expect(
convertToMentionPath(
"C:\\Users\\user\\project\\file\\\\ with\\\\ spaces.txt",
"C:\\Users\\user\\project",
),
).toBe(
// Expected output has double-escaped spaces (the result of two rounds of escaping)
"@/file//\\ with//\\ spaces.txt",
)

// Same test with Unix path format
expect(
convertToMentionPath("/Users/user/project/file\\\\ with\\\\ spaces.txt", "/Users/user/project"),
).toBe("@/file//\\ with//\\ spaces.txt")
})
})
})
Loading