Файли
openidea/src/features/tools/__tests__/serverSettings.test.tsx
T
胡翀таzzxzz12345 ac426c8881 feat(python): Add ty and select the Python language server (#121)
ty (`ty server`) joins basedpyright and Pyright as a Python language server (fallback, gets the project venv like the others, installable from Tools). Settings › Language Servers gets a Python language server picker: Automatic (first available), basedpyright, Pyright, ty or None, written through the per-server enabled settings (null globally / true in a project for enabled, false for disabled); other combinations show as Custom combination. Picker text is translated; the hint sits on its own line.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Co-authored-by: zzxzz12345 <[email protected]>
2026-10-28 14:57:34 +08:00

210 рядки
9.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useProject } from "@/platform/project";
import { provideNotificationService } from "@/workbench/contributions";
import { MemoryToolsBackend, sampleTools, setToolsBackend } from "../backend";
import { LanguageServersPage } from "../LanguageServersPage";
import { resetLanguageChoices } from "../ServerSettings";
import { useTools } from "../store";
import { formatArgs, nameFromCommand, parseArgs, parseEnv, parseJsonObject, parseList } from "../serverConfig";
import { resetTestForm, stagedFor, TestSettingsForm, useTestForm } from "./settingsForm";
describe("server settings text fields", () => {
it("splits arguments like a shell and formats them back", () => {
expect(parseArgs(`--stdio --log "a b" 'c"d' e\\ f`)).toEqual(["--stdio", "--log", "a b", 'c"d', "e f"]);
expect(parseArgs(" ")).toBeNull();
expect(parseArgs('""')).toEqual([""]);
expect(() => parseArgs('"open')).toThrow(/Unclosed/);
const args = ["--x", "a b", 'q"t', ""];
expect(parseArgs(formatArgs(args))).toEqual(args);
});
it("parses environment variables separated by semicolons", () => {
expect(parseEnv("A=1; B_2 = x=y;\n C=")).toEqual({ A: "1", B_2: "x=y", C: "" });
expect(parseEnv(" ; ")).toBeNull();
expect(() => parseEnv("A=1; nope")).toThrow("Expected NAME=value: nope");
});
it("accepts JSON objects without nulls", () => {
expect(parseJsonObject('{ "a": { "b": [1] } }')).toEqual({ a: { b: [1] } });
expect(parseJsonObject(" ")).toBeNull();
expect(() => parseJsonObject("[1]")).toThrow("Expected a JSON object");
expect(() => parseJsonObject('{ "a": { "b": null } }')).toThrow("at a.b");
expect(() => parseJsonObject("{")).toThrow();
});
it("splits lists and names servers after their executable", () => {
expect(parseList("python, typescript python")).toEqual(["python", "typescript"]);
expect(parseList(",")).toBeNull();
expect(nameFromCommand("/opt/bin/ruff")).toBe("ruff");
expect(nameFromCommand("C:\\tools\\my server.exe")).toBe("my-server");
});
});
describe("Language Servers page settings", () => {
let backend: MemoryToolsBackend;
const listItem = (name: string) =>
screen.getByRole("list", { name: /language servers/i }).querySelector<HTMLElement>(`[data-tool="${name}"]`);
const details = (name: string) => screen.getByLabelText(`${name} details`);
const renderPage = () =>
render(
<TestSettingsForm>
<LanguageServersPage />
</TestSettingsForm>,
);
beforeEach(() => {
provideNotificationService({ notify: () => {} });
backend = new MemoryToolsBackend(
sampleTools().map((t) =>
t.name === "rust-analyzer"
? { ...t, defaultArgs: ["--stdio"], defaultInitializationOptions: '{"checkOnSave":true}' }
: t,
),
1,
);
setToolsBackend(backend);
resetLanguageChoices();
resetTestForm();
useTools.setState({ tools: [], loaded: false, loading: false, error: null, jobs: {}, logs: {} });
useProject.setState({ project: null, settings: null });
});
afterEach(() => setToolsBackend(null));
it("selects exactly one Python analyzer and restores automatic fallback without writing defaults", async () => {
const user = userEvent.setup();
renderPage();
const picker = screen.getByRole("button", { name: "Python language server" });
expect(picker).toHaveTextContent("Automatic");
await user.click(picker);
await user.click(screen.getByRole("option", { name: "ty" }));
expect(stagedFor("languageServers.basedpyright.enabled")).toBe(false);
expect(stagedFor("languageServers.pyright.enabled")).toBe(false);
expect(stagedFor("languageServers.ty.enabled")).toBeNull();
expect(picker).toHaveTextContent("ty");
await user.click(picker);
await user.click(screen.getByRole("option", { name: /Automatic/ }));
for (const name of ["basedpyright", "pyright", "ty"]) {
expect(stagedFor(`languageServers.${name}.enabled`)).toBeNull();
expect(useTestForm.getState().values).not.toHaveProperty(`languageServers.${name}.enabled`);
}
});
it("spells out the chosen Python analyzer in project settings", async () => {
const user = userEvent.setup();
render(
<TestSettingsForm scope="project">
<LanguageServersPage />
</TestSettingsForm>,
);
const picker = screen.getByRole("button", { name: "Python language server" });
await user.click(picker);
await user.click(screen.getByRole("option", { name: "Pyright" }));
expect(stagedFor("languageServers.basedpyright.enabled")).toBe(false);
expect(stagedFor("languageServers.pyright.enabled")).toBe(true);
expect(stagedFor("languageServers.ty.enabled")).toBe(false);
expect(picker).toHaveTextContent("Pyright");
await user.click(picker);
await user.click(screen.getByRole("option", { name: /Automatic/ }));
for (const name of ["basedpyright", "pyright", "ty"]) {
expect(stagedFor(`languageServers.${name}.enabled`)).toBe(true);
}
});
it("stages startup options, showing the defaults they replace", async () => {
const user = userEvent.setup();
renderPage();
const pane = await screen.findByLabelText("rust-analyzer details");
// Folded while nothing is set there.
expect(within(pane).queryByRole("textbox", { name: "Arguments" })).toBeNull();
await user.click(within(pane).getByRole("button", { name: "Startup" }));
const args = within(pane).getByRole("textbox", { name: "Arguments" });
expect(args).toHaveAttribute("placeholder", "--stdio");
expect(within(pane).getByRole("textbox", { name: "Initialization options" })).toHaveAttribute(
"placeholder",
JSON.stringify({ checkOnSave: true }, null, 2),
);
await user.type(args, '--log "a b"');
expect(stagedFor("languageServers.rust-analyzer.args")).toEqual(["--log", "a b"]);
await user.type(within(pane).getByRole("textbox", { name: "Environment variables" }), "RA_LOG=info");
expect(stagedFor("languageServers.rust-analyzer.env")).toEqual({ RA_LOG: "info" });
// Invalid JSON is marked and not staged; valid JSON is.
const init = within(pane).getByRole("textbox", { name: "Initialization options" });
await user.click(init);
await user.paste('{ "check": null }');
expect(within(pane).getByRole("alert")).toHaveTextContent("null is not supported");
expect(stagedFor("languageServers.rust-analyzer.initializationOptions")).toBeUndefined();
await user.clear(init);
await user.paste('{ "check": { "command": "clippy" } }');
expect(stagedFor("languageServers.rust-analyzer.initializationOptions")).toEqual({ check: { command: "clippy" } });
// Turned off: marked in the list.
await user.click(within(pane).getByRole("checkbox", { name: /Use rust-analyzer for Rust files/ }));
expect(stagedFor("languageServers.rust-analyzer.enabled")).toBe(false);
expect(listItem("rust-analyzer")).toHaveTextContent("Disabled");
await user.click(within(pane).getByRole("button", { name: "Restore Defaults" }));
expect(stagedFor("languageServers.rust-analyzer")).toBeNull();
expect(listItem("rust-analyzer")).not.toHaveTextContent("Disabled");
expect(args).toHaveValue("");
});
it("adds a server of one's own from its command line, then removes it", async () => {
const user = userEvent.setup();
renderPage();
await screen.findByLabelText("rust-analyzer details");
await user.click(screen.getByRole("button", { name: "Add Language Server…" }));
const dialog = screen.getByRole("dialog", { name: "New Language Server" });
const ok = within(dialog).getByRole("button", { name: "OK" });
await user.type(within(dialog).getByRole("textbox", { name: "Command" }), "/opt/bin/ruff server");
expect(within(dialog).getByRole("textbox", { name: "Name" })).toHaveValue("ruff");
expect(ok).toBeDisabled();
expect(dialog).toHaveTextContent("Choose the languages it serves.");
const picker = within(dialog).getByRole("button", { name: "Languages" });
expect(picker).toHaveTextContent("Choose languages…");
await user.click(picker);
await user.click(await screen.findByRole("checkbox", { name: "Python" }));
await user.keyboard("{Escape}");
expect(picker).toHaveTextContent("Python");
await user.click(ok);
expect(stagedFor("languageServers.ruff")).toEqual({
command: "/opt/bin/ruff",
args: ["server"],
languages: ["python"],
});
// Listed and selected before Apply, as a new server.
await waitFor(() => expect(listItem("ruff")).toHaveAttribute("aria-current", "true"));
expect(listItem("ruff")).toHaveTextContent("New");
const pane = details("ruff");
expect(within(pane).getByRole("textbox", { name: "Command" })).toHaveValue("/opt/bin/ruff");
expect(within(pane).getByRole("textbox", { name: "Arguments" })).toHaveValue("server");
expect(pane).toHaveTextContent("Checked when the settings are applied.");
// Edits go into the staged entry.
await user.type(within(pane).getByRole("textbox", { name: "Root markers" }), "pyproject.toml");
expect(useTestForm.getState().values).toMatchObject({
languageServers: { ruff: { rootMarkers: ["pyproject.toml"] } },
});
await user.click(screen.getByRole("button", { name: "Remove Language Server" }));
expect(stagedFor("languageServers.ruff")).toBeNull();
expect(listItem("ruff")).toBeNull();
});
it("keeps the remove button for servers of one's own", async () => {
const user = userEvent.setup();
resetTestForm({ languageServers: { ruff: { command: "ruff", languages: ["python"] } } });
renderPage();
await screen.findByLabelText("rust-analyzer details");
expect(screen.getByRole("button", { name: "Remove Language Server" })).toBeDisabled();
await user.click(listItem("ruff")!);
expect(screen.getByRole("button", { name: "Remove Language Server" })).toBeEnabled();
});
});