Skip to content

Ghost Provider Tests #1336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 8 additions & 1 deletion src/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export default [
"no-undef": "off",
},
},
{
files: ["**/__test_cases__/**/*"],
rules: {
"no-undef": "off",
"no-const-assign": "off",
},
},
{
ignores: ["webview-ui", "out"],
},
]
]
230 changes: 230 additions & 0 deletions src/services/autocomplete/__tests__/MockTextDocument.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { describe, it, expect } from "vitest"
import * as vscode from "vscode"
import { MockTextDocument } from "./MockTextDocument"

describe("MockTextDocument", () => {
describe("constructor and basic properties", () => {
it("should create document from single line content", () => {
const doc = new MockTextDocument("const x = 1")

expect(doc.lineCount).toBe(1)
expect(doc.getText()).toBe("const x = 1")
})

it("should create document from multi-line content", () => {
const content = "function test() {\n return true\n}"
const doc = new MockTextDocument(content)

expect(doc.lineCount).toBe(3)
expect(doc.getText()).toBe(content)
})

it("should handle empty content", () => {
const doc = new MockTextDocument("")

expect(doc.lineCount).toBe(1)
expect(doc.getText()).toBe("")
})

it("should handle content with only newlines", () => {
const doc = new MockTextDocument("\n\n\n")

expect(doc.lineCount).toBe(4)
expect(doc.getText()).toBe("\n\n\n")
})
})

describe("getText() method", () => {
const multiLineContent = "line 1\nline 2\nline 3\nline 4"
let doc: MockTextDocument

beforeEach(() => {
doc = new MockTextDocument(multiLineContent)
})

it("should return full text when no range provided", () => {
expect(doc.getText()).toBe(multiLineContent)
})

it("should return text within single line range", () => {
const range = new vscode.Range(new vscode.Position(1, 2), new vscode.Position(1, 5))

expect(doc.getText(range)).toBe("ne ")
})

it("should return text from start of line to position", () => {
const range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 4))

expect(doc.getText(range)).toBe("line")
})

it("should return text from position to end of line", () => {
const range = new vscode.Range(new vscode.Position(1, 5), new vscode.Position(1, 6))

expect(doc.getText(range)).toBe("2")
})

it("should return text across multiple lines", () => {
const range = new vscode.Range(new vscode.Position(1, 2), new vscode.Position(3, 2))

expect(doc.getText(range)).toBe("ne 2\nline 3\nli")
})

it("should handle range starting from middle of first line", () => {
const range = new vscode.Range(new vscode.Position(0, 2), new vscode.Position(2, 4))

expect(doc.getText(range)).toBe("ne 1\nline 2\nline")
})

it("should handle range ending in middle of last line", () => {
const range = new vscode.Range(new vscode.Position(1, 0), new vscode.Position(2, 4))

expect(doc.getText(range)).toBe("line 2\nline")
})

it("should handle range beyond document bounds gracefully", () => {
const range = new vscode.Range(new vscode.Position(2, 0), new vscode.Position(10, 10))

expect(doc.getText(range)).toBe("line 3\nline 4")
})
})

describe("lineAt() method", () => {
const content = " const x = 1\n\n function test() {\n return x\n }"
let doc: MockTextDocument

beforeEach(() => {
doc = new MockTextDocument(content)
})

it("should return correct line information for first line", () => {
const line = doc.lineAt(0)

expect(line.text).toBe(" const x = 1")
expect(line.lineNumber).toBe(0)
expect(line.firstNonWhitespaceCharacterIndex).toBe(2)
expect(line.isEmptyOrWhitespace).toBe(false)
})

it("should return correct line information for empty line", () => {
const line = doc.lineAt(1)

expect(line.text).toBe("")
expect(line.lineNumber).toBe(1)
expect(line.firstNonWhitespaceCharacterIndex).toBe(0)
expect(line.isEmptyOrWhitespace).toBe(true)
})

it("should return correct line information for whitespace-only line", () => {
const docWithWhitespace = new MockTextDocument("line1\n \nline3")
const line = docWithWhitespace.lineAt(1)

expect(line.text).toBe(" ")
expect(line.lineNumber).toBe(1)
expect(line.firstNonWhitespaceCharacterIndex).toBe(4)
expect(line.isEmptyOrWhitespace).toBe(true)
})

it("should return correct line information for indented line", () => {
const line = doc.lineAt(2)

expect(line.text).toBe(" function test() {")
expect(line.lineNumber).toBe(2)
expect(line.firstNonWhitespaceCharacterIndex).toBe(4)
expect(line.isEmptyOrWhitespace).toBe(false)
})

it("should include correct range information", () => {
const line = doc.lineAt(0)

expect(line.range.start.line).toBe(0)
expect(line.range.start.character).toBe(0)
expect(line.range.end.line).toBe(0)
expect(line.range.end.character).toBe(13) // Length of " const x = 1"
})

it("should throw error for invalid line number (negative)", () => {
expect(() => doc.lineAt(-1)).toThrow("Invalid line number: -1")
})

it("should throw error for invalid line number (beyond bounds)", () => {
expect(() => doc.lineAt(10)).toThrow("Invalid line number: 10")
})
})

describe("edge cases and special characters", () => {
it("should handle tabs correctly", () => {
const doc = new MockTextDocument("\tfunction test() {\n\t\treturn true\n\t}")

expect(doc.lineCount).toBe(3)

const line0 = doc.lineAt(0)
expect(line0.text).toBe("\tfunction test() {")
expect(line0.firstNonWhitespaceCharacterIndex).toBe(1)

const line1 = doc.lineAt(1)
expect(line1.text).toBe("\t\treturn true")
expect(line1.firstNonWhitespaceCharacterIndex).toBe(2)
})

it("should handle mixed whitespace", () => {
const doc = new MockTextDocument(" \t const x = 1")
const line = doc.lineAt(0)

expect(line.text).toBe(" \t const x = 1")
expect(line.firstNonWhitespaceCharacterIndex).toBe(5)
expect(line.isEmptyOrWhitespace).toBe(false)
})

it("should handle unicode characters", () => {
const doc = new MockTextDocument("const 🚀 = 'rocket'\nconst 中文 = 'chinese'")

expect(doc.lineCount).toBe(2)
expect(doc.lineAt(0).text).toBe("const 🚀 = 'rocket'")
expect(doc.lineAt(1).text).toBe("const 中文 = 'chinese'")
})

it("should handle Windows line endings (CRLF)", () => {
const doc = new MockTextDocument("line1\r\nline2\r\nline3")

// Note: split("\n") will still work but will include \r in the text
expect(doc.lineCount).toBe(3)
expect(doc.lineAt(0).text).toBe("line1\r")
expect(doc.lineAt(1).text).toBe("line2\r")
expect(doc.lineAt(2).text).toBe("line3")
})
})

describe("integration with vscode types", () => {
it("should work with vscode.Range for getText", () => {
const doc = new MockTextDocument("function test() {\n return 42\n}")

// Create a range using vscode constructors
const start = new vscode.Position(0, 9)
const end = new vscode.Position(1, 11)
const range = new vscode.Range(start, end)

expect(doc.getText(range)).toBe("test() {\n return ")
})

it("should return TextLine compatible with vscode interface", () => {
const doc = new MockTextDocument(" const value = 'test'")
const line = doc.lineAt(0)

// Verify it has all required TextLine properties
expect(line).toHaveProperty("text")
expect(line).toHaveProperty("range")
expect(line).toHaveProperty("lineNumber")
expect(line).toHaveProperty("rangeIncludingLineBreak")
expect(line).toHaveProperty("firstNonWhitespaceCharacterIndex")
expect(line).toHaveProperty("isEmptyOrWhitespace")

// Verify types match vscode expectations
expect(typeof line.text).toBe("string")
expect(typeof line.lineNumber).toBe("number")
expect(typeof line.firstNonWhitespaceCharacterIndex).toBe("number")
expect(typeof line.isEmptyOrWhitespace).toBe("boolean")
expect(line.range).toBeInstanceOf(vscode.Range)
})
})
})
69 changes: 69 additions & 0 deletions src/services/autocomplete/__tests__/MockTextDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as vscode from "vscode"

/**
* A simulated vscode TextDocument for testing.
*/
export class MockTextDocument {
private contentLines: string[]

constructor(content: string) {
this.contentLines = content.split("\n")
}

updateContent(newContent: string): void {
this.contentLines = newContent.split("\n")
}

getText(range?: vscode.Range): string {
if (!range) {
return this.contentLines.join("\n")
}

const startLine = range.start.line
const endLine = range.end.line

if (startLine === endLine) {
return this.contentLines[startLine].substring(range.start.character, range.end.character)
}

const lines: string[] = []
for (let i = startLine; i <= endLine && i < this.contentLines.length; i++) {
if (i === startLine) {
lines.push(this.contentLines[i].substring(range.start.character))
} else if (i === endLine) {
lines.push(this.contentLines[i].substring(0, range.end.character))
} else {
lines.push(this.contentLines[i])
}
}

return lines.join("\n")
}

get lineCount(): number {
return this.contentLines.length
}

/**
* Returns information about a specific line in the document
* @param lineNumber The zero-based line number
* @returns A simplified TextLine object containing the text and position information
*/
lineAt(lineNumber: number): vscode.TextLine {
if (lineNumber < 0 || lineNumber >= this.contentLines.length) {
throw new Error(`Invalid line number: ${lineNumber}`)
}

const text = this.contentLines[lineNumber]
const range = new vscode.Range(new vscode.Position(lineNumber, 0), new vscode.Position(lineNumber, text.length))

return {
text,
range,
lineNumber,
rangeIncludingLineBreak: range,
firstNonWhitespaceCharacterIndex: text.search(/\S|$/),
isEmptyOrWhitespace: !/\S/.test(text),
} as vscode.TextLine
}
}
64 changes: 64 additions & 0 deletions src/services/autocomplete/__tests__/MockTextEditor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest"
import { MockTextEditor, CURSOR_MARKER } from "./MockTextEditor"

describe("MockTextEditor", () => {
it("should correctly parse cursor position from marker", () => {
const editor = MockTextEditor.create(`function test() {\n ␣return true\n}`)

// Test selection property
expect(editor.selection.active.line).toBe(1)
expect(editor.selection.active.character).toBe(4)
expect(editor.selection.anchor.line).toBe(1)
expect(editor.selection.anchor.character).toBe(4)

// Verify the cursor marker was removed from the document
const documentText = editor.document.getText()
expect(documentText).toBe("function test() {\n return true\n}")
expect(documentText).not.toContain("␣")
})

it("should handle cursor at start of document", () => {
const editor = MockTextEditor.create(`␣const x = 1`)

expect(editor.selection.active.line).toBe(0)
expect(editor.selection.active.character).toBe(0)
expect(editor.document.getText()).toBe("const x = 1")
})

it("should handle cursor at end of document", () => {
const editor = MockTextEditor.create(`const x = 1␣`)

expect(editor.selection.active.line).toBe(0)
expect(editor.selection.active.character).toBe(11)
expect(editor.document.getText()).toBe("const x = 1")
})

it("should handle cursor in middle of line", () => {
const editor = MockTextEditor.create(`const ␣x = 1`)

expect(editor.selection.active.line).toBe(0)
expect(editor.selection.active.character).toBe(6)
expect(editor.document.getText()).toBe("const x = 1")
})

it("should default to position (0,0) when cursor marker is missing", () => {
const editor = MockTextEditor.create("const x = 1")

// Test selection property
expect(editor.selection.active.line).toBe(0)
expect(editor.selection.active.character).toBe(0)
expect(editor.selection.anchor.line).toBe(0)
expect(editor.selection.anchor.character).toBe(0)

// Verify document content is unchanged
expect(editor.document.getText()).toBe("const x = 1")
})

it("should provide access to document line information", () => {
const editor = MockTextEditor.create(`function test() {\n ␣return true\n}`)

const line = editor.document.lineAt(editor.selection.active.line)
expect(line.text).toBe(" return true")
expect(line.lineNumber).toBe(1)
})
})
Loading