// Formatting Unit Tests — formatHover(), formatDefinition(), // formatReferences(), formatCompletions(), formatDocumentSymbols(), // formatDiagnostics(). // // These functions are defined inside index.ts (not exported), so we // copy their logic here for testing. In a real project these would be // extracted to a shared module; for now we test the expected shapes // of LSP responses against known inputs. import { describe, it } from "node:test"; import * as assert from "node:assert/strict"; // uriToPath is used by formatDefinition and formatReferences. import { uriToPath } from "../../src/client.ts"; // --- Re-implement the formatting functions for testing --- // These mirror the logic in index.ts. If index.ts changes, update these. function formatHover(result: unknown): string { if (!result || typeof result !== "object") return "(no hover info)"; const hover = result as { contents?: unknown }; if (!hover.contents) return "(empty)"; const contents = hover.contents as any; if ( "value" in contents && typeof contents.value === "string" ) { return contents.value; } if (Array.isArray(hover.contents)) { return hover.contents .map((s: any) => (typeof s === "string" ? s : (s?.value ?? ""))) .join("\n"); } if ( "value" in contents && typeof contents.language === "string" ) { const ms = hover.contents as any; return `\`\`\`${ms.language}\n${ms.value}\n\`\`\``; } return JSON.stringify(result, null, 2); } function formatDefinition(result: unknown): string { if (!result) return "(no definition found)"; const locations = Array.isArray(result) ? result : [result]; if (locations.length === 0) return "(no definition found)"; return locations .map((loc: any, i: number) => { const file = uriToPath(loc.uri); const range = loc.range; return `${i + 1}. ${file} (${range.start.line + 1}:${range.start.character + 1})`; }) .join("\n"); } function formatReferences(result: unknown): string { if (!result || !Array.isArray(result)) return "(no references found)"; if (result.length === 0) return "(no references found)"; return result .map((loc: any, i: number) => { const file = uriToPath(loc.uri); const range = loc.range; return `${i + 1}. ${file} (${range.start.line + 1}:${range.start.character + 1})`; }) .join("\n"); } function formatDiagnostics(result: unknown): string { if (!result || !("diagnostics" in (result as object))) return "(no diagnostics)"; const diags = (result as any).diagnostics; if (!Array.isArray(diags) || diags.length === 0) return "(no diagnostics)"; const severityNames: Record = { 1: "Error", 2: "Warning", 3: "Info", 4: "Hint", }; return diags .map((d: any, i: number) => { const sev = severityNames[d.severity] ?? `sev:${d.severity}`; const range = d.range; const line = range?.start?.line != null ? range.start.line + 1 : "?"; const col = range?.start?.character != null ? range.start.character + 1 : "?"; return `${i + 1}. [${sev}] ${d.message} (line ${line}, col ${col})`; }) .join("\n"); } // --- Tests --- describe("formatHover", () => { it("returns MarkupContent value directly", () => { const result = { contents: { kind: "markdown", value: "**bold** text" } }; assert.strictEqual(formatHover(result), "**bold** text"); }); it("joins MarkedString array", () => { const result = { contents: ["line1", "line2"] }; assert.strictEqual(formatHover(result), "line1\nline2"); }); it("returns value for object with language and value (first branch matches)", () => { // The first branch ("value" is string) matches before the language check, // so this returns the raw value. Matches actual index.ts behavior. const result = { contents: { language: "typescript", value: "const x: number" }, }; assert.strictEqual(formatHover(result), "const x: number"); }); it("returns fallback for null result", () => { assert.strictEqual(formatHover(null), "(no hover info)"); }); it("returns empty for missing contents", () => { assert.strictEqual(formatHover({}), "(empty)"); }); }); describe("formatDefinition", () => { it("formats a single location", () => { const result = { uri: "file:///home/user/src/index.ts", range: { start: { line: 5, character: 10 }, end: { line: 5, character: 20 } }, }; const output = formatDefinition(result); assert.ok(output.includes("/home/user/src/index.ts")); assert.ok(output.includes("6:11")); // 1-indexed }); it("formats multiple locations", () => { const result = [ { uri: "file:///a.ts", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, }, { uri: "file:///b.ts", range: { start: { line: 10, character: 0 }, end: { line: 10, character: 5 } }, }, ]; const output = formatDefinition(result); assert.ok(output.includes("1. /a.ts (1:1)")); assert.ok(output.includes("2. /b.ts (11:1)")); }); it("returns fallback for null result", () => { assert.strictEqual(formatDefinition(null), "(no definition found)"); }); }); describe("formatReferences", () => { it("formats reference locations", () => { const result = [ { uri: "file:///src/main.ts", range: { start: { line: 3, character: 5 }, end: { line: 3, character: 10 } }, }, ]; const output = formatReferences(result); assert.ok(output.includes("1. /src/main.ts (4:6)")); }); it("returns fallback for empty array", () => { assert.strictEqual(formatReferences([]), "(no references found)"); }); it("returns fallback for null result", () => { assert.strictEqual(formatReferences(null), "(no references found)"); }); }); describe("formatDiagnostics", () => { it("formats diagnostic messages with severity", () => { const result = { uri: "file:///src/broken.ts", diagnostics: [ { severity: 1, message: "Type 'string' is not assignable to type 'number'.", range: { start: { line: 0, character: 7 }, end: { line: 0, character: 20 } }, }, ], }; const output = formatDiagnostics(result); assert.ok(output.includes("[Error]")); assert.ok(output.includes("line 1, col 8")); }); it("returns fallback for no diagnostics", () => { assert.strictEqual(formatDiagnostics({}), "(no diagnostics)"); assert.strictEqual(formatDiagnostics(null), "(no diagnostics)"); assert.strictEqual(formatDiagnostics({ diagnostics: [] }), "(no diagnostics)"); }); });