Files
kandev/apps/web/lib/lsp/lsp-progress-view.test.ts
T
2026-08-05 10:57:24 +01:00

193 lines
5.7 KiB
TypeScript

import { expect, it } from "vitest";
import { activateLocale } from "@/lib/i18n";
import type { LspProgressSnapshot } from "./lsp-progress";
import {
formatLspElapsed,
getLspCompactSummary,
getLspConnectionLabel,
getLspLifecycleAction,
getLspProgressView,
} from "./lsp-progress-view";
const EMPTY_PROGRESS: LspProgressSnapshot = {
initializingSince: null,
active: [],
completed: null,
hasReportedProgress: false,
};
it("formats elapsed time without estimating time remaining", () => {
expect(formatLspElapsed(-1)).toBe("0 sec");
expect(formatLspElapsed(9_900)).toBe("9 sec");
expect(formatLspElapsed(65_000)).toBe("1 min 05 sec");
expect(formatLspElapsed(3_725_000)).toBe("1 hr 02 min");
});
it("localizes elapsed units and their composition", async () => {
try {
await activateLocale("pseudo");
expect(formatLspElapsed(65_000)).toBe("1 ḿĩń 05 śēć");
} finally {
await activateLocale("en");
}
});
it("formats compact live summaries for the application status bar", () => {
expect(
getLspCompactSummary(
{ state: "starting" },
{ ...EMPTY_PROGRESS, initializingSince: 2_000 },
67_000,
),
).toBe("Server process started · 1 min 05 sec");
expect(
getLspCompactSummary(
{ state: "ready" },
{
...EMPTY_PROGRESS,
active: [
{
token: "import",
title: "Importing project",
message: "Resolving modules",
percentage: 42,
startedAt: 2_000,
},
],
},
67_000,
),
).toBe("Importing project · 42%");
expect(getLspCompactSummary({ state: "ready" }, EMPTY_PROGRESS, 67_000)).toBe("Connected");
});
it("keeps connection labels and lifecycle actions separate from project work", () => {
expect(getLspConnectionLabel({ state: "ready" })).toBe("Connected");
expect(getLspConnectionLabel({ state: "installing" })).toBe("Installing language server");
expect(getLspConnectionLabel({ state: "error", reason: "crashed" })).toBe("Error");
expect(getLspLifecycleAction({ state: "disabled" })).toEqual({
label: "Start",
enabled: true,
});
expect(getLspLifecycleAction({ state: "ready" })).toEqual({
label: "Stop",
enabled: true,
});
expect(getLspLifecycleAction({ state: "error", reason: "crashed" })).toEqual({
label: "Retry",
enabled: true,
});
expect(getLspLifecycleAction({ state: "stopping" })).toEqual({
label: "Stopping",
enabled: false,
});
});
it("presents the oldest active server item without averaging concurrent work", () => {
const progress: LspProgressSnapshot = {
initializingSince: 1_000,
active: [
{
token: "first",
title: "Importing project",
message: "Resolving modules",
percentage: 42,
startedAt: 2_000,
},
{
token: "second",
title: "Scanning dependencies",
message: null,
percentage: 90,
startedAt: 3_000,
},
],
completed: null,
hasReportedProgress: true,
};
expect(getLspProgressView({ state: "starting" }, progress, 67_000)).toEqual({
kind: "active",
title: "Importing project",
message: "Resolving modules",
percentage: 42,
elapsed: "1 min 05 sec",
concurrentCount: 2,
});
});
it("discloses that the server process launched while initialize is pending", () => {
const progress = { ...EMPTY_PROGRESS, initializingSince: 2_000 };
expect(getLspConnectionLabel({ state: "starting" }, progress)).toBe("Server process started");
expect(getLspProgressView({ state: "starting" }, progress, 61_999, "kotlin")).toEqual({
kind: "initializing",
stage: "initialize_pending",
title: "Server process started",
description: "Waiting for the language server to respond to the LSP initialize request.",
guidance: "Cross-file features become available after initialization completes.",
elapsed: "59 sec",
});
expect(getLspLifecycleAction({ state: "starting" })).toEqual({
label: "Stop",
enabled: true,
});
});
it("warns at 60 seconds without timing out or inventing Kotlin progress", () => {
const view = getLspProgressView(
{ state: "starting" },
{ ...EMPTY_PROGRESS, initializingSince: 2_000 },
62_000,
"kotlin",
);
expect(view).toEqual({
kind: "initializing",
stage: "long_running",
title: "Initialization is taking longer than usual",
description: "The server process is still running while Kandev waits for LSP initialize.",
guidance:
"Kotlin LSP may be importing the Gradle project. Cross-file features remain unavailable until initialization completes.",
elapsed: "1 min 00 sec",
});
expect(JSON.stringify(view).toLowerCase()).not.toMatch(/eta|time remaining|percentage|indexing/);
expect(getLspLifecycleAction({ state: "starting" })).toEqual({
label: "Stop",
enabled: true,
});
});
it("states when a ready server has not reported background analysis", () => {
expect(getLspProgressView({ state: "ready" }, EMPTY_PROGRESS, 10_000)).toEqual({
kind: "idle",
title: "No background work reported",
description:
"The language server has not reported ongoing project analysis. Cross-file results may still warm up.",
});
});
it("describes completion as server-reported work rather than full indexing", () => {
const progress: LspProgressSnapshot = {
...EMPTY_PROGRESS,
hasReportedProgress: true,
completed: {
token: 1,
title: "Project import",
message: "Import finished",
startedAt: 1_000,
completedAt: 3_000,
},
};
expect(getLspProgressView({ state: "ready" }, progress, 4_000)).toEqual({
kind: "completed",
title: "Server-reported work finished",
workTitle: "Project import",
message: "Import finished",
});
});