The helper's own contract — marker handling, requested position, label extraction and the rejection when the marker is absent — is asserted against in-memory documents whose contextual types are written inline, so the helper is tested without coupling to the library's code.
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
/* 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";
|
|
|
|
import {
|
|
type CompletionResult,
|
|
type CompletionTarget,
|
|
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
|
|
// (development/testing.md § Test helpers).
|
|
test("test-helper home: `#test-utils/…` resolves to the helper and types it", () => {
|
|
// Arrange
|
|
const target: CompletionTarget = {
|
|
file: "src/util/__tests__/lsp-completion.ts",
|
|
source: "",
|
|
};
|
|
|
|
// Assert — no session is constructed: the constructor spawns `tsc --lsp`,
|
|
// so only the resolved values and their types are checked here.
|
|
expectTypeOf(LspSession).toBeConstructibleWith("repo-root");
|
|
expectTypeOf<CompletionResult>().toMatchTypeOf<{
|
|
labels: readonly string[];
|
|
}>();
|
|
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<CompletionResult> => {
|
|
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<Promise<CompletionResult>>();
|
|
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<CompletionResult["labels"]>().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<CompletionResult>();
|
|
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<CompletionResult>();
|
|
return assert.rejects(probed, /marker not found/);
|
|
});
|