feat: move terminal settings, look and ssh tools into ssh-terminal plugin

This commit is contained in:
LukeGus committed 2026-09-29 02:38:37 -05:00
1 parent ffa872ebd3
commit d9bba10612
242 files changed
+25071 -7207

No files matched your search

@@ -104,4 +104,64 @@ describe(`${manifest.id} activate`, () => {
}),
).resolves.toBe(false);
});
it("owns the SSH Tools panel and its rail item", async () => {
rendered = await renderWithApp(plugin, { manifest, locales });
expect(rendered.registered.panels()).toContain("ssh-tools");
expect(rendered.registered.railItems().map((item) => item.id)).toContain(
"ssh-tools",
);
});
it("registers its settings components", async () => {
rendered = await renderWithApp(plugin, { manifest, locales });
expect(rendered.registered.settingsComponents()).toEqual(
expect.arrayContaining([
"terminalDefaults",
"newHostTheme",
"newHostFontFamily",
]),
);
});
it("offers the terminal look to other plugins through actions", async () => {
rendered = await renderWithApp(plugin, {
manifest,
locales,
api: {
get: async () => ({ data: { user: { terminalDefaults: {} } } }),
} as never,
});
const look = (await rendered.app.invokeAction("terminal.resolveTheme", {
host: {
id: "1",
name: "h",
ip: "10.0.0.1",
port: 22,
pluginSettings: {
"ssh-terminal": { inheritAppearance: false, theme: "dracula" },
},
},
appTheme: "dark",
})) as { themeId: string; colors: { background: string } };
expect(look.themeId).toBe("dracula");
expect(look.colors.background).toBe("#282a36");
const themes = (await rendered.app.invokeAction("terminal.themes")) as {
id: string;
}[];
expect(themes.map((theme) => theme.id)).toContain("dracula");
expect(themes.map((theme) => theme.id)).not.toContain("termixDark");
});
it("adds the terminal font faces while active and removes them on deactivate", async () => {
const app = await renderWithApp(plugin, { manifest, locales });
expect(
document.head.querySelector("style[data-termix-terminal-styles]"),
).not.toBeNull();
await app.deactivate();
expect(
document.head.querySelector("style[data-termix-terminal-styles]"),
).toBeNull();
});
});
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import {
getNextTerminalFontSize,
getTerminalFontZoomDirection,
} from "../../../src/frontend/look/terminal-font-zoom";
function keyEvent(
overrides: Partial<KeyboardEvent>,
): Pick<KeyboardEvent, "altKey" | "code" | "ctrlKey" | "key" | "metaKey"> {
return {
altKey: false,
code: "",
ctrlKey: false,
key: "",
metaKey: false,
...overrides,
};
}
describe("getTerminalFontZoomDirection", () => {
it("recognizes Ctrl+Plus and Ctrl+Minus", () => {
expect(
getTerminalFontZoomDirection(
keyEvent({ ctrlKey: true, code: "Equal", key: "+" }),
),
).toBe(1);
expect(
getTerminalFontZoomDirection(
keyEvent({ ctrlKey: true, code: "Minus", key: "-" }),
),
).toBe(-1);
});
it("recognizes unshifted equal, numpad, and macOS shortcuts", () => {
expect(
getTerminalFontZoomDirection(
keyEvent({ ctrlKey: true, code: "Equal", key: "=" }),
),
).toBe(1);
expect(
getTerminalFontZoomDirection(
keyEvent({ ctrlKey: true, code: "NumpadAdd", key: "+" }),
),
).toBe(1);
expect(
getTerminalFontZoomDirection(
keyEvent({ metaKey: true, code: "NumpadSubtract", key: "-" }),
),
).toBe(-1);
});
it("ignores unmodified and Alt-modified keys", () => {
expect(
getTerminalFontZoomDirection(keyEvent({ code: "Equal", key: "+" })),
).toBe(0);
expect(
getTerminalFontZoomDirection(
keyEvent({ altKey: true, ctrlKey: true, code: "Minus", key: "-" }),
),
).toBe(0);
});
});
describe("getNextTerminalFontSize", () => {
it("changes the font size one pixel at a time", () => {
expect(getNextTerminalFontSize(14, 1)).toBe(15);
expect(getNextTerminalFontSize(14, -1)).toBe(13);
});
it("keeps the existing zoom limits", () => {
expect(getNextTerminalFontSize(36, 1)).toBe(36);
expect(getNextTerminalFontSize(8, -1)).toBe(8);
});
});
@@ -0,0 +1,89 @@
import { describe, expect, it, vi } from "vitest";
import {
ensureTerminalFontsLoaded,
installTerminalGlobalStyles,
} from "../../../src/frontend/look/terminal-global-styles";
describe("installTerminalGlobalStyles", () => {
it("adds the font faces while the plugin runs and removes them after", () => {
const dispose = installTerminalGlobalStyles();
const style = document.head.querySelector(
"style[data-termix-terminal-styles]",
);
expect(style?.innerHTML).toContain("Caskaydia Cove Nerd Font Mono");
dispose();
expect(
document.head.querySelector("style[data-termix-terminal-styles]"),
).toBeNull();
});
});
describe("ensureTerminalFontsLoaded", () => {
it("requests regular, bold, italic, and bold-italic variants for the given font", () => {
const load = vi.fn().mockResolvedValue([]);
const originalFonts = document.fonts;
Object.defineProperty(document, "fonts", {
configurable: true,
value: { load },
});
try {
ensureTerminalFontsLoaded("Caskaydia Cove Nerd Font Mono");
expect(load).toHaveBeenCalledWith(
'400 16px "Caskaydia Cove Nerd Font Mono"',
);
expect(load).toHaveBeenCalledWith(
'700 16px "Caskaydia Cove Nerd Font Mono"',
);
expect(load).toHaveBeenCalledWith(
'italic 400 16px "Caskaydia Cove Nerd Font Mono"',
);
expect(load).toHaveBeenCalledWith(
'italic 700 16px "Caskaydia Cove Nerd Font Mono"',
);
expect(load).toHaveBeenCalledTimes(4);
} finally {
Object.defineProperty(document, "fonts", {
configurable: true,
value: originalFonts,
});
}
});
it("does not throw when document.fonts is unavailable", () => {
const originalFonts = document.fonts;
Object.defineProperty(document, "fonts", {
configurable: true,
value: undefined,
});
try {
expect(() => ensureTerminalFontsLoaded("JetBrains Mono")).not.toThrow();
} finally {
Object.defineProperty(document, "fonts", {
configurable: true,
value: originalFonts,
});
}
});
it("swallows rejected font load promises", async () => {
const load = vi.fn().mockRejectedValue(new Error("network error"));
const originalFonts = document.fonts;
Object.defineProperty(document, "fonts", {
configurable: true,
value: { load },
});
try {
expect(() => ensureTerminalFontsLoaded("Fira Code")).not.toThrow();
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
Object.defineProperty(document, "fonts", {
configurable: true,
value: originalFonts,
});
}
});
});
@@ -0,0 +1,88 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { HostTerminalSettings } from "../../../src/frontend/settings/HostTerminalSettings";
globalThis.ResizeObserver ??= class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
afterEach(cleanup);
type Form = Record<string, unknown>;
/** The switch on the setting row labelled with this key. */
function switchFor(label: string): HTMLElement {
const row = screen.getByText(label).closest("div.justify-between")!;
return row.querySelector("button")!;
}
function renderSettings(form: Form = { pluginSettings: {} }) {
let current: Form = form;
const setField = vi.fn();
const updateForm = vi.fn((patch: (form: Form) => Form) => {
current = patch(current);
});
render(
<HostTerminalSettings
form={form}
setField={setField}
updateForm={updateForm}
host={{ id: "1", name: "box", ip: "10.0.0.1", port: 22 }}
snippets={[{ id: 7, name: "deploy" }]}
/>,
);
return { setField, updateForm, current: () => current };
}
describe("HostTerminalSettings", () => {
it("renders the appearance and behavior cards", () => {
renderSettings();
expect(screen.getByText("hosts.terminalAppearance")).toBeTruthy();
expect(screen.getByText("hosts.behaviorAndAdvanced")).toBeTruthy();
});
it("leaves the SSH connection options to the core SSH tab", () => {
renderSettings();
expect(screen.queryByText("hosts.keepaliveIntervalLabel")).toBeNull();
expect(screen.queryByText("hosts.environmentVariablesLabel")).toBeNull();
expect(screen.queryByText("hosts.sshAgentForwardingLabel")).toBeNull();
});
it("draws the auto-fill switches that left the SSH tab", () => {
renderSettings();
expect(screen.getByText("hosts.passwordPromptAutoFillLabel")).toBeTruthy();
expect(screen.getByText("hosts.sudoPasswordAutoFillLabel")).toBeTruthy();
});
it("sets the startup snippet, a core host field, from the offered snippets", () => {
const { setField } = renderSettings();
const select = screen
.getByRole("option", { name: "deploy" })
.closest("select")!;
fireEvent.change(select, { target: { value: "7" } });
expect(setField).toHaveBeenCalledWith("startupSnippetId", 7);
});
it("writes its values into the host's ssh-terminal settings", () => {
const { current } = renderSettings({
pluginSettings: { other: { keep: true } },
});
fireEvent.click(switchFor("hosts.enableAutoMosh"));
expect(current().pluginSettings).toEqual({
other: { keep: true },
"ssh-terminal": { autoMosh: true },
});
});
it("takes the host off the user's look when the look changes", () => {
const { current } = renderSettings({
pluginSettings: { "ssh-terminal": { inheritAppearance: false } },
});
fireEvent.click(switchFor("hosts.cursorBlinking"));
expect(
(current().pluginSettings as Record<string, Form>)["ssh-terminal"],
).toMatchObject({ cursorBlink: false, inheritAppearance: false });
});
});
@@ -0,0 +1,37 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { TerminalDefaultsSettings } from "../../../src/frontend/settings/TerminalDefaultsSettings";
afterEach(cleanup);
describe("TerminalDefaultsSettings", () => {
it("counts the defaults the user set", () => {
render(
<TerminalDefaultsSettings
pluginId="ssh-terminal"
values={{ terminalDefaults: { fontSize: 18, cursorBlink: false } }}
setValue={vi.fn()}
running
/>,
);
expect(screen.getByText("2")).toBeTruthy();
});
it("applies the dialog's values to the terminalDefaults setting", () => {
const setValue = vi.fn();
render(
<TerminalDefaultsSettings
pluginId="ssh-terminal"
values={{ terminalDefaults: { fontSize: 18 } }}
setValue={setValue}
running
/>,
);
fireEvent.click(screen.getByText("terminalDefaults.manage"));
fireEvent.change(screen.getByDisplayValue("18"), {
target: { value: "20" },
});
fireEvent.click(screen.getByText("terminalDefaults.apply"));
expect(setValue).toHaveBeenCalledWith("terminalDefaults", { fontSize: 20 });
});
});
@@ -0,0 +1,62 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { moveLocalTerminalPreferences } from "../../../src/frontend/settings/local-preferences-migration";
import { resetTerminalClientSettings } from "../../../src/frontend/terminal-settings";
function api(user: Record<string, unknown>) {
return {
get: vi.fn(async () => ({ data: { user } })),
put: vi.fn(async () => ({ data: {} })),
};
}
beforeEach(() => {
localStorage.clear();
resetTerminalClientSettings();
});
afterEach(() => resetTerminalClientSettings());
describe("moveLocalTerminalPreferences", () => {
it("does nothing when the browser kept nothing", async () => {
const client = api({});
await moveLocalTerminalPreferences({ api: client as never });
expect(client.get).not.toHaveBeenCalled();
});
it("moves the browser's values into settings still at their default, once", async () => {
localStorage.setItem("terminalLocalEchoMode", "on");
localStorage.setItem("terminalLinkClickBehavior", "direct");
localStorage.setItem("commandAutocomplete", "true");
const client = api({ commandAutocomplete: false });
await moveLocalTerminalPreferences({ api: client as never });
expect(client.put).toHaveBeenCalledWith("/user-settings", {
localEcho: "on",
linkClickBehavior: "direct",
commandAutocomplete: true,
});
expect(localStorage.getItem("terminalLocalEchoMode")).toBeNull();
expect(localStorage.getItem("commandAutocomplete")).toBeNull();
});
it("keeps a value the user already changed on the server", async () => {
localStorage.setItem("terminalLocalEchoMode", "off");
const client = api({ localEcho: "on" });
await moveLocalTerminalPreferences({ api: client as never });
expect(client.put).not.toHaveBeenCalled();
expect(localStorage.getItem("terminalLocalEchoMode")).toBeNull();
});
it("keeps the browser values for next time when the server is unreachable", async () => {
localStorage.setItem("terminalLocalEchoMode", "on");
const client = {
get: vi.fn(async () => {
throw new Error("offline");
}),
put: vi.fn(),
};
await moveLocalTerminalPreferences({ api: client as never });
expect(localStorage.getItem("terminalLocalEchoMode")).toBe("on");
});
});
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import {
fromTriState,
toTriState,
} from "../../../src/frontend/settings/tri-state";
describe("tri-state", () => {
it("keeps unset booleans distinct from false", () => {
expect(toTriState(undefined)).toBe("inherit");
expect(toTriState(true)).toBe("on");
expect(toTriState(false)).toBe("off");
expect(fromTriState("inherit")).toBeUndefined();
expect(fromTriState("on")).toBe(true);
expect(fromTriState("off")).toBe(false);
});
});
@@ -0,0 +1,67 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
act,
cleanup,
fireEvent,
render,
screen,
} from "@testing-library/react";
import { SshToolsPanel } from "../../../src/frontend/ssh-tools/SshToolsPanel";
import { registerSession } from "../../../src/frontend/session-registry";
import type { TerminalHandle } from "../../../src/frontend/terminal/terminal-types";
const disposers: Array<() => void> = [];
afterEach(() => {
cleanup();
for (const dispose of disposers.splice(0)) dispose();
});
function openSession(id: string, label: string) {
const handle = { sendInput: vi.fn() } as unknown as TerminalHandle;
act(() => {
disposers.push(registerSession({ id, hostId: 1, label }, handle));
});
return handle;
}
function renderPanel(targetTab?: { id: string }) {
return render(
<SshToolsPanel
targetTab={targetTab as never}
active
shell={{} as never}
setEditing={() => {}}
placement="left"
/>,
);
}
describe("SshToolsPanel", () => {
it("says so when no terminal is open", () => {
renderPanel();
expect(screen.getByText("sshTools.noTerminalTabsOpen")).toBeTruthy();
});
it("lists open terminals and follows sessions opening later", () => {
openSession("tab-1", "web-01");
renderPanel();
expect(screen.getByText("web-01")).toBeTruthy();
openSession("tab-2", "db-01");
expect(screen.getByText("db-01")).toBeTruthy();
});
it("types into the selected terminals only", () => {
const first = openSession("tab-1", "web-01");
const second = openSession("tab-2", "db-01");
renderPanel({ id: "tab-1" });
fireEvent.click(screen.getByText(/sshTools.startRecording/));
fireEvent.keyDown(
screen.getByPlaceholderText("sshTools.broadcastInputPlaceholder"),
{ key: "a" },
);
expect(first.sendInput).toHaveBeenCalledWith("a");
expect(second.sendInput).not.toHaveBeenCalled();
});
});