diff --git a/src/util/__tests__/lsp-completion.test.ts b/src/util/__tests__/lsp-completion.test.ts index fcaab51..c02f74e 100644 --- a/src/util/__tests__/lsp-completion.test.ts +++ b/src/util/__tests__/lsp-completion.test.ts @@ -1,5 +1,6 @@ /* oxlint-disable typescript/no-floating-promises -- expectTypeOf() is a sync type-assertion library that the type-aware linter misidentifies as a promise */ import { strict as assert } from "node:assert"; +import path from "node:path"; import { test } from "node:test"; import { expectTypeOf } from "expect-type"; @@ -10,6 +11,8 @@ import { LspSession, } from "#test-utils/lsp-completion.ts"; +const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); + // The `#test-utils/*` self-reference (package.json#imports) is the one route // scattered test files take to reach test helpers; this pins that it resolves // and types without starting a language server @@ -30,3 +33,116 @@ test("test-helper home: `#test-utils/…` resolves to the helper and types it", assert.equal(typeof LspSession, "function"); assert.equal(target.file, "src/util/__tests__/lsp-completion.ts"); }); + +// The helper drives a language server, so its contract is asserted against the +// server. Every document below is probed from memory and carries its +// contextual type inline, so the helper is tested without the library's code. +const probe = (source: string, marker?: string): Promise => { + const session = new LspSession(REPO_ROOT); + const target: CompletionTarget = + marker === undefined + ? { file: "src/__probe.ts", source } + : { file: "src/__probe.ts", source, marker }; + return session.completionLabelsAt(target).finally(() => session.close()); +}; + +const HANDLERS = [ + "type Handlers = { a: () => number; b: () => number; _?: () => number };", + "declare const apply: (handlers: Handlers) => Handlers;", +]; + +test("helper: a fresh object literal completes with its contextual keys", () => { + // Arrange + const source = [ + ...HANDLERS, + "const done = apply({", + " /*COMPLETE*/", + "});", + "export { done };", + ].join("\n"); + + // Act + const probed = probe(source); + + // Assert — the position is the marker's, which the helper strips + expectTypeOf(probed).toEqualTypeOf>(); + return probed.then((result) => { + assert.deepEqual(result.position, { line: 3, character: 4 }); + assert.deepEqual([...result.labels], ["_?", "a", "b"]); + }); +}); + +test("helper: a handled key drops out of the popup", () => { + // Arrange + const source = [ + ...HANDLERS, + "const done = apply({", + " b: () => 1,", + " /*COMPLETE*/", + "});", + "export { done };", + ].join("\n"); + + // Act + const probed = probe(source); + + // Assert + expectTypeOf().toEqualTypeOf< + readonly string[] + >(); + return probed.then((result) => { + assert.deepEqual([...result.labels], ["_?", "a"]); + }); +}); + +test("helper: a custom marker is located at the line start", () => { + // Arrange + const source = [ + "type Handlers = { a: () => number; _?: () => number };", + "declare const apply: (handlers: Handlers) => Handlers;", + "const done = apply({", + "@@@", + "});", + "export { done };", + ].join("\n"); + + // Act + const probed = probe(source, "@@@"); + + // Assert + expectTypeOf(probed).resolves.toEqualTypeOf(); + return probed.then((result) => { + assert.deepEqual(result.position, { line: 3, character: 0 }); + assert.deepEqual([...result.labels], ["_?", "a"]); + }); +}); + +test("helper: labels are read from the server, not from a pattern literal", () => { + // Arrange + const source = [ + 'const text = "x";', + "const upper = text./*COMPLETE*/;", + "export { upper };", + ].join("\n"); + + // Act + const probed = probe(source); + + // Assert + expectTypeOf(probed).resolves.toHaveProperty("labels"); + return probed.then((result) => { + assert.ok(result.labels.includes("toUpperCase")); + }); +}); + +test("helper: a source without the marker rejects", () => { + // Arrange + const source = "const text = 1;\nexport { text };\n"; + + // Act + const probed = probe(source); + + // Assert + expectTypeOf(probed).resolves.toEqualTypeOf(); + return assert.rejects(probed, /marker not found/); +});