mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: move terminal settings, look and ssh tools into ssh-terminal plugin
This commit is contained in:
1 parent
ffa872ebd3
commit
d9bba10612
242 files changed
+25071
-7207
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
type TestDb,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import type { PluginHostSummary } from "@termix/plugin-sdk/backend";
|
||||
import manifestJson from "../../manifest.json";
|
||||
|
||||
export const pluginDir = fileURLToPath(new URL("../..", import.meta.url));
|
||||
@@ -32,7 +33,11 @@ export interface TestServer {
|
||||
* the way core mounts it, with the acting user taken from a test header.
|
||||
*/
|
||||
export async function startServer(
|
||||
options: { permissions?: string[]; settings?: Record<string, unknown> } = {},
|
||||
options: {
|
||||
permissions?: string[];
|
||||
settings?: Record<string, unknown>;
|
||||
hosts?: PluginHostSummary[];
|
||||
} = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir);
|
||||
for (const user of ["user-1", "user-2"]) {
|
||||
@@ -51,6 +56,7 @@ export async function startServer(
|
||||
router: () => (router = express.Router()),
|
||||
permissions: options.permissions,
|
||||
settings: options.settings,
|
||||
hosts: options.hosts,
|
||||
});
|
||||
|
||||
const { activate } = await import("../../src/backend/index.js");
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hostImportNormalizer,
|
||||
hostPayloadLegacy,
|
||||
} from "../../src/backend/host-import";
|
||||
|
||||
describe("hostImportNormalizer", () => {
|
||||
it("reads a 2.8 export's terminalConfig into the host settings", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
enableTerminal: false,
|
||||
terminalConfig: {
|
||||
fontSize: 16,
|
||||
autoTmux: true,
|
||||
keepaliveInterval: 30,
|
||||
startupSnippetId: 2,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
enableTerminal: false,
|
||||
inheritAppearance: false,
|
||||
fontSize: 16,
|
||||
autoTmux: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the raw JSON a SQLite export carries", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
terminalConfig: JSON.stringify({ localEcho: "on" }),
|
||||
}),
|
||||
).toEqual({ localEcho: "on" });
|
||||
});
|
||||
|
||||
it("leaves terminalConfig alone when the export carries the plugin's own values", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
pluginSettings: { "ssh-terminal": { autoTmux: false } },
|
||||
terminalConfig: { autoTmux: true },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("imports nothing from a row without terminal values", () => {
|
||||
expect(hostImportNormalizer({ name: "x" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostPayloadLegacy", () => {
|
||||
it("puts the settings back in the 2.8 terminalConfig shape", () => {
|
||||
const { terminalConfig } = hostPayloadLegacy({
|
||||
autoTmux: true,
|
||||
inheritAppearance: true,
|
||||
fontSize: 20,
|
||||
}) as { terminalConfig: Record<string, unknown> };
|
||||
expect(terminalConfig.autoTmux).toBe(true);
|
||||
expect(terminalConfig).not.toHaveProperty("fontSize");
|
||||
expect(terminalConfig).not.toHaveProperty("localEcho");
|
||||
});
|
||||
|
||||
it("includes the look of a host that has its own", () => {
|
||||
const { terminalConfig } = hostPayloadLegacy({
|
||||
inheritAppearance: false,
|
||||
fontSize: 20,
|
||||
}) as { terminalConfig: Record<string, unknown> };
|
||||
expect(terminalConfig.fontSize).toBe(20);
|
||||
expect(terminalConfig.theme).toBe("termix");
|
||||
});
|
||||
});
|
||||
@@ -132,3 +132,78 @@ describe("client settings and image storage", () => {
|
||||
expect(result.body.connected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("terminal settings routes", () => {
|
||||
const host = (id: number, userId = "user-1") => ({
|
||||
id,
|
||||
userId,
|
||||
name: `h${id}`,
|
||||
ip: "10.0.0.1",
|
||||
port: 22,
|
||||
username: "root",
|
||||
tags: null,
|
||||
folder: null,
|
||||
authType: "password",
|
||||
});
|
||||
|
||||
it("hands every user their own terminal settings and the new-host defaults", async () => {
|
||||
server = await startServer({
|
||||
settings: { newHostFontSize: 20, newHostTheme: "nord" },
|
||||
});
|
||||
await server.mock.ctx.settings.setUser(
|
||||
"user-1",
|
||||
"commandAutocomplete",
|
||||
true,
|
||||
);
|
||||
await server.mock.ctx.settings.setUser("user-1", "terminalDefaults", {
|
||||
fontSize: 18,
|
||||
});
|
||||
const { body } = await server.request("GET", "/client-settings");
|
||||
expect(body.user).toMatchObject({
|
||||
commandAutocomplete: true,
|
||||
terminalDefaults: { fontSize: 18 },
|
||||
localEcho: "auto",
|
||||
linkClickBehavior: "confirm",
|
||||
});
|
||||
expect(body.newHostDefaults).toMatchObject({ fontSize: 20, theme: "nord" });
|
||||
const other = await server.request("GET", "/client-settings", {
|
||||
user: "user-2",
|
||||
});
|
||||
expect(other.body.user.commandAutocomplete).toBe(false);
|
||||
});
|
||||
|
||||
it("saves the browser preferences a user moves over, for that user only", async () => {
|
||||
server = await startServer();
|
||||
const saved = await server.request("PUT", "/user-settings", {
|
||||
body: { localEcho: "on", linkClickBehavior: "direct", theme: "x" },
|
||||
});
|
||||
expect(saved.status).toBe(200);
|
||||
expect(await server.mock.ctx.settings.getUser("user-1", "localEcho")).toBe(
|
||||
"on",
|
||||
);
|
||||
expect(
|
||||
await server.mock.ctx.settings.getUser("user-2", "localEcho"),
|
||||
).not.toBe("on");
|
||||
});
|
||||
|
||||
it("turns auto tmux on for a host the user can edit", async () => {
|
||||
server = await startServer({ hosts: [host(1)] });
|
||||
const saved = await server.request("PUT", "/hosts/1/auto-tmux", {
|
||||
body: { enabled: true },
|
||||
});
|
||||
expect(saved.status).toBe(200);
|
||||
expect(await server.mock.ctx.settings.getHost(1, "autoTmux")).toBe(true);
|
||||
});
|
||||
|
||||
it("refuses auto tmux on a host the user cannot reach, or a bad body", async () => {
|
||||
server = await startServer({ hosts: [host(1)] });
|
||||
const unknown = await server.request("PUT", "/hosts/2/auto-tmux", {
|
||||
body: { enabled: true },
|
||||
});
|
||||
expect(unknown.status).toBe(403);
|
||||
const bad = await server.request("PUT", "/hosts/1/auto-tmux", {
|
||||
body: { enabled: "yes" },
|
||||
});
|
||||
expect(bad.status).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_HOST_TERMINAL_SETTINGS,
|
||||
hostSettingsFromTerminalConfig,
|
||||
readHostTerminalSettings,
|
||||
readUserSettings,
|
||||
resolveTerminalSettings,
|
||||
} from "../../src/shared/terminal-settings";
|
||||
|
||||
describe("hostSettingsFromTerminalConfig", () => {
|
||||
it("keeps behavior and leaves a look-less host following the user", () => {
|
||||
expect(
|
||||
hostSettingsFromTerminalConfig({
|
||||
autoMosh: true,
|
||||
fastScrollModifier: "ctrl",
|
||||
keepaliveInterval: 5,
|
||||
}),
|
||||
).toEqual({ autoMosh: true, fastScrollModifier: "ctrl" });
|
||||
});
|
||||
|
||||
it("drops values a select field would refuse", () => {
|
||||
expect(
|
||||
hostSettingsFromTerminalConfig({ bellStyle: "loud", cursorStyle: "bar" }),
|
||||
).toEqual({ inheritAppearance: false, cursorStyle: "bar" });
|
||||
});
|
||||
|
||||
it("reads nothing from garbage", () => {
|
||||
expect(hostSettingsFromTerminalConfig("{")).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTerminalSettings", () => {
|
||||
it("uses the user's defaults while the host follows them", () => {
|
||||
const host = readHostTerminalSettings({ fontSize: 30, autoTmux: true });
|
||||
const resolved = resolveTerminalSettings(host, { fontSize: 18 });
|
||||
expect(resolved.fontSize).toBe(18);
|
||||
expect(resolved.autoTmux).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the host's own look once it opts out", () => {
|
||||
const host = readHostTerminalSettings({
|
||||
inheritAppearance: false,
|
||||
fontSize: 30,
|
||||
});
|
||||
expect(resolveTerminalSettings(host, { fontSize: 18 }).fontSize).toBe(30);
|
||||
});
|
||||
|
||||
it("falls back to the built-in defaults", () => {
|
||||
expect(resolveTerminalSettings(null).theme).toBe(
|
||||
DEFAULT_HOST_TERMINAL_SETTINGS.theme,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readUserSettings", () => {
|
||||
it("types what it reads and drops broken saved themes", () => {
|
||||
expect(
|
||||
readUserSettings({
|
||||
terminalDefaults: { fontSize: "16", junk: 1 },
|
||||
customThemes: [{ id: "a", name: "A", colors: {} }, { id: 1 }],
|
||||
localEcho: "sideways",
|
||||
linkClickBehavior: "direct",
|
||||
}),
|
||||
).toEqual({
|
||||
terminalDefaults: { fontSize: 16 },
|
||||
customThemes: [{ id: "a", name: "A", colors: {} }],
|
||||
commandAutocomplete: false,
|
||||
localEcho: "auto",
|
||||
linkClickBehavior: "direct",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user