mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55: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
@@ -65,8 +65,7 @@ describe("omitOwnerSshAuthFromSharedEdit", () => {
|
||||
expect(sharedEdit.name).toBe(payload.name);
|
||||
expect(sharedEdit.ip).toBe("10.0.0.42");
|
||||
expect(sharedEdit.notes).toBe("editable");
|
||||
expect(sharedEdit.terminalConfig?.sudoPassword).toBeUndefined();
|
||||
expect(sharedEdit.terminalConfig?.agentSocketPath).toBeUndefined();
|
||||
expect(sharedEdit.sshOptions?.agentSocketPath).toBeUndefined();
|
||||
for (const field of [
|
||||
"authType",
|
||||
"credentialId",
|
||||
@@ -169,10 +168,7 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload.sshOptions as unknown as Record<string, unknown> | null;
|
||||
|
||||
expect(tc?.agentSocketPath).toBe("/run/user/1000/gnupg/S.gpg-agent.ssh");
|
||||
expect(payload.password).toBeNull();
|
||||
@@ -187,10 +183,7 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload.sshOptions as unknown as Record<string, unknown> | null;
|
||||
|
||||
expect(tc?.agentSocketPath).toBeNull();
|
||||
});
|
||||
@@ -204,15 +197,12 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload.sshOptions as unknown as Record<string, unknown> | null;
|
||||
|
||||
expect(tc?.agentSocketPath).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves agentIdentity in terminalConfig when authType is agent", () => {
|
||||
it("preserves agentIdentity in sshOptions when authType is agent", () => {
|
||||
const form = {
|
||||
...createHostEditorForm(null),
|
||||
authType: "agent" as const,
|
||||
@@ -220,10 +210,7 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload.sshOptions as unknown as Record<string, unknown> | null;
|
||||
|
||||
expect(tc?.agentIdentity).toBe(
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA test-key",
|
||||
@@ -239,15 +226,12 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload.sshOptions as unknown as Record<string, unknown> | null;
|
||||
|
||||
expect(tc?.agentIdentity).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps agentIdentity in terminalConfig for shared edits (not owner-private)", () => {
|
||||
it("keeps agentIdentity in sshOptions for shared edits (not owner-private)", () => {
|
||||
const form = {
|
||||
...createHostEditorForm(null),
|
||||
authType: "agent" as const,
|
||||
@@ -257,39 +241,10 @@ describe("buildHostEditorPayload auth field isolation", () => {
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const sharedEdit = omitOwnerSshAuthFromSharedEdit(payload);
|
||||
|
||||
expect(sharedEdit.terminalConfig?.agentIdentity).toBe(
|
||||
expect(sharedEdit.sshOptions?.agentIdentity).toBe(
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA test-key",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves sudo password autofill settings", () => {
|
||||
const form = {
|
||||
...createHostEditorForm(null),
|
||||
sudoPasswordAutoFill: true,
|
||||
sudoPassword: "sudo-secret",
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
|
||||
expect(tc?.sudoPasswordAutoFill).toBe(true);
|
||||
expect(tc?.sudoPassword).toBe("sudo-secret");
|
||||
});
|
||||
|
||||
it("defaults password prompt autofill on and saves it when turned off", () => {
|
||||
expect(createHostEditorForm(null).passwordPromptAutoFill).toBe(true);
|
||||
|
||||
const form = {
|
||||
...createHostEditorForm(null),
|
||||
passwordPromptAutoFill: false,
|
||||
};
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
|
||||
expect(payload.terminalConfig?.passwordPromptAutoFill).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sudo password persistence indicator", () => {
|
||||
@@ -305,10 +260,7 @@ describe("sudo password persistence indicator", () => {
|
||||
const form = { ...createHostEditorForm(host) };
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload as unknown as Record<string, unknown>;
|
||||
|
||||
expect(tc?.sudoPassword).toBeUndefined();
|
||||
expect(JSON.parse(JSON.stringify(tc))).not.toHaveProperty("sudoPassword");
|
||||
@@ -322,15 +274,12 @@ describe("sudo password persistence indicator", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload as unknown as Record<string, unknown>;
|
||||
|
||||
expect(tc?.sudoPassword).toBe("new-sudo-pass");
|
||||
});
|
||||
|
||||
it("sends null to explicitly clear a saved sudo password", () => {
|
||||
it("sends an empty value to explicitly clear a saved sudo password", () => {
|
||||
const host = { hasSudoPassword: true } as Host;
|
||||
const form = {
|
||||
...createHostEditorForm(host),
|
||||
@@ -338,12 +287,9 @@ describe("sudo password persistence indicator", () => {
|
||||
};
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
const tc = payload.terminalConfig as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const tc = payload as unknown as Record<string, unknown>;
|
||||
|
||||
expect(tc?.sudoPassword).toBeNull();
|
||||
expect(tc?.sudoPassword).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -434,43 +380,6 @@ describe("RDP/VNC/Telnet password persistence indicator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("user connection defaults", () => {
|
||||
const defaults = {
|
||||
terminal: { fontSize: 18, cursorBlink: false },
|
||||
};
|
||||
|
||||
it("shows inherited values without persisting them as host overrides", () => {
|
||||
const host = {
|
||||
enableSsh: true,
|
||||
terminalConfig: { autoTmux: true },
|
||||
} as unknown as Host;
|
||||
const form = createHostEditorForm(host, undefined, defaults);
|
||||
|
||||
expect(form).toMatchObject({
|
||||
fontSize: 18,
|
||||
cursorBlink: false,
|
||||
inheritTerminalAppearance: true,
|
||||
});
|
||||
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
expect(payload.terminalConfig).toMatchObject({ autoTmux: true });
|
||||
expect(payload.terminalConfig).not.toHaveProperty("fontSize");
|
||||
});
|
||||
|
||||
it("keeps explicit host overrides above user defaults", () => {
|
||||
const host = {
|
||||
enableSsh: true,
|
||||
terminalConfig: { fontSize: 12 },
|
||||
} as unknown as Host;
|
||||
const form = createHostEditorForm(host, undefined, defaults);
|
||||
|
||||
expect(form).toMatchObject({
|
||||
fontSize: 12,
|
||||
inheritTerminalAppearance: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createHostEditorForm credentialId", () => {
|
||||
it("coerces a numeric credentialId to a string so credential lookups match", () => {
|
||||
const form = createHostEditorForm({
|
||||
@@ -489,20 +398,6 @@ describe("createHostEditorForm credentialId", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createHostEditorForm auto-tmux", () => {
|
||||
it("inherits the admin default for a new host but keeps an existing host's own choice", () => {
|
||||
expect(createHostEditorForm(null, { autoTmux: true }).autoTmux).toBe(true);
|
||||
expect(createHostEditorForm(null, {}).autoTmux).toBe(false);
|
||||
|
||||
const host = {
|
||||
id: "1",
|
||||
name: "box",
|
||||
terminalConfig: { autoTmux: false },
|
||||
} as unknown as Host;
|
||||
expect(createHostEditorForm(host, { autoTmux: true }).autoTmux).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Support#1240: RDP/VNC/Telnet can now originate from the desktop, so the
|
||||
// control has to appear for hosts that enable only those protocols -- it used
|
||||
// to be gated on SSH alone.
|
||||
@@ -532,18 +427,44 @@ describe("connectionOriginAppliesTo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("macOS Option character defaults", () => {
|
||||
it("leaves Option available for keyboard-layout characters by default", () => {
|
||||
const form = createHostEditorForm(null);
|
||||
expect(form.macOptionIsMeta).toBe(false);
|
||||
expect(
|
||||
buildHostEditorPayload(form, sshOnly).terminalConfig?.macOptionIsMeta,
|
||||
).toBe(false);
|
||||
describe("terminal fields", () => {
|
||||
it("sends only the startup snippet in terminalConfig, and the SSH options on their own", () => {
|
||||
const form = {
|
||||
...createHostEditorForm(null),
|
||||
startupSnippetId: 4,
|
||||
keepaliveInterval: 30,
|
||||
agentForwarding: true,
|
||||
environmentVariables: [{ key: "LANG", value: "C" }],
|
||||
};
|
||||
const payload = buildHostEditorPayload(form, sshOnly);
|
||||
|
||||
expect(payload.terminalConfig).toEqual({ startupSnippetId: 4 });
|
||||
expect(payload.sshOptions).toMatchObject({
|
||||
keepaliveInterval: 30,
|
||||
keepaliveCountMax: 5,
|
||||
allowLegacyAlgorithms: true,
|
||||
agentForwarding: true,
|
||||
environmentVariables: [{ key: "LANG", value: "C" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an explicitly saved Meta preference", () => {
|
||||
const host = { terminalConfig: { macOptionIsMeta: true } } as Host;
|
||||
expect(createHostEditorForm(host).macOptionIsMeta).toBe(true);
|
||||
it("reads a host's SSH options back into the form", () => {
|
||||
const host = {
|
||||
sshOptions: { keepaliveInterval: 15, allowLegacyAlgorithms: false },
|
||||
terminalConfig: { startupSnippetId: 2 },
|
||||
} as unknown as Host;
|
||||
const form = createHostEditorForm(host);
|
||||
|
||||
expect(form.keepaliveInterval).toBe(15);
|
||||
expect(form.allowLegacyAlgorithms).toBe(false);
|
||||
expect(form.startupSnippetId).toBe(2);
|
||||
});
|
||||
|
||||
it("leaves the terminal fields out with SSH off", () => {
|
||||
const payload = buildHostEditorPayload(createHostEditorForm(null), rdpOnly);
|
||||
expect(payload).not.toHaveProperty("terminalConfig");
|
||||
expect(payload).not.toHaveProperty("sshOptions");
|
||||
expect(payload).not.toHaveProperty("sudoPassword");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { HostTerminalSettings } from "@/sidebar/HostTerminalSettings";
|
||||
import { createHostEditorForm } from "@/sidebar/HostEditorData";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock("@/api/open-tabs-api", () => ({
|
||||
getUserPreferences: vi.fn().mockResolvedValue({ customThemes: null }),
|
||||
saveUserPreferences: vi.fn(),
|
||||
parseCustomThemes: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("@/components/theme-provider", () => ({
|
||||
useTheme: () => ({ theme: "dark" }),
|
||||
}));
|
||||
|
||||
globalThis.ResizeObserver ??= class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
} as unknown as typeof ResizeObserver;
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function renderSettings(setField = vi.fn()) {
|
||||
const form = createHostEditorForm(undefined);
|
||||
render(
|
||||
<HostTerminalSettings
|
||||
form={form}
|
||||
setField={setField}
|
||||
snippets={[{ id: 7, name: "deploy" }]}
|
||||
/>,
|
||||
);
|
||||
return setField;
|
||||
}
|
||||
|
||||
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 keepalive fields to the SSH tab", () => {
|
||||
renderSettings();
|
||||
expect(screen.queryByText("hosts.keepaliveIntervalLabel")).toBeNull();
|
||||
});
|
||||
|
||||
it("sets the startup snippet 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);
|
||||
});
|
||||
});
|
||||
@@ -52,13 +52,12 @@ describe("RAIL_ITEMS", () => {
|
||||
// "port-forwarding" to the tunnels plugin's, "serial" to the serial
|
||||
// plugin's, "collab" to the session-sharing plugin's, and "session-logs"
|
||||
// to the session-recording plugin's, "termix-id" to the termix-identity
|
||||
// plugin's.
|
||||
// plugin's, "ssh-tools" to the ssh-terminal plugin's.
|
||||
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
|
||||
"hosts",
|
||||
"credentials",
|
||||
"connections",
|
||||
"quick-connect",
|
||||
"ssh-tools",
|
||||
"macros",
|
||||
"sync",
|
||||
]);
|
||||
@@ -70,10 +69,10 @@ describe("RAIL_ITEMS", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("marks exactly the three mobile primary slots", () => {
|
||||
it("marks core's mobile primary slots", () => {
|
||||
expect(
|
||||
RAIL_ITEMS.filter((item) => item.mobilePrimary).map((item) => item.id),
|
||||
).toEqual(["hosts", "quick-connect", "ssh-tools"]);
|
||||
).toEqual(["hosts", "quick-connect"]);
|
||||
});
|
||||
|
||||
it("marks the panels that can open as a tab", () => {
|
||||
@@ -81,7 +80,7 @@ describe("RAIL_ITEMS", () => {
|
||||
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
||||
.filter((item) => item.promotable)
|
||||
.map((item) => item.id),
|
||||
).toEqual(["ssh-tools", "macros"]);
|
||||
).toEqual(["macros"]);
|
||||
});
|
||||
|
||||
it("derives promotableIds from the promotable flag", () => {
|
||||
@@ -118,7 +117,7 @@ describe("RAIL_ITEMS", () => {
|
||||
});
|
||||
|
||||
it("only offers reference panels in the right dock", () => {
|
||||
expect(rightDockableIds()).toEqual(["connections", "ssh-tools", "macros"]);
|
||||
expect(rightDockableIds()).toEqual(["connections", "macros"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user