[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
391
domain/terminalAppearance.test.ts
Normal file
391
domain/terminalAppearance.test.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
applyCustomAccentToTerminalTheme,
|
||||
applySessionFontSizeToHost,
|
||||
getFollowAppTerminalThemeIds,
|
||||
getFollowAppTerminalThemeSelectionUpdate,
|
||||
getFollowAppThemePickExpectedSettledId,
|
||||
isFollowAppTerminalThemeId,
|
||||
isFollowAppThemePickSettled,
|
||||
mergeTerminalHostUpdate,
|
||||
resolveFollowedTerminalThemeId,
|
||||
resolveFollowAppTerminalThemeId,
|
||||
resolveManualTerminalThemeId,
|
||||
TERMINAL_THEME_AUTO,
|
||||
} from "./terminalAppearance";
|
||||
import type { Host, TerminalTheme } from "./models";
|
||||
|
||||
const baseTheme: TerminalTheme = {
|
||||
id: "ui-snow",
|
||||
name: "Snow",
|
||||
type: "light",
|
||||
colors: {
|
||||
background: "#f1f4f8",
|
||||
foreground: "#24292f",
|
||||
cursor: "#0969da",
|
||||
selection: "#add6ff",
|
||||
black: "#24292f",
|
||||
red: "#cf222e",
|
||||
green: "#116329",
|
||||
yellow: "#9a6700",
|
||||
blue: "#0969da",
|
||||
magenta: "#8250df",
|
||||
cyan: "#0e7574",
|
||||
white: "#6e7781",
|
||||
brightBlack: "#57606a",
|
||||
brightRed: "#a40e26",
|
||||
brightGreen: "#1a7f37",
|
||||
brightYellow: "#7d4e00",
|
||||
brightBlue: "#218bff",
|
||||
brightMagenta: "#a475f9",
|
||||
brightCyan: "#0c7875",
|
||||
brightWhite: "#8c959f",
|
||||
},
|
||||
};
|
||||
|
||||
test("applies a custom accent to terminal cursor and selection colors", () => {
|
||||
const accented = applyCustomAccentToTerminalTheme(baseTheme, "custom", "160 70% 40%");
|
||||
|
||||
assert.notEqual(accented, baseTheme);
|
||||
assert.equal(accented.colors.cursor, "#1fad7e");
|
||||
assert.equal(accented.colors.selection, "#b1f1dc");
|
||||
assert.equal(baseTheme.colors.cursor, "#0969da");
|
||||
assert.equal(baseTheme.colors.selection, "#add6ff");
|
||||
});
|
||||
|
||||
test("keeps terminal theme unchanged without a valid custom accent", () => {
|
||||
assert.equal(applyCustomAccentToTerminalTheme(baseTheme, "theme", "160 70% 40%"), baseTheme);
|
||||
assert.equal(applyCustomAccentToTerminalTheme(baseTheme, "custom", "not-a-color"), baseTheme);
|
||||
});
|
||||
|
||||
const savedHost: Host = {
|
||||
id: "host-1",
|
||||
label: "Core switch",
|
||||
hostname: "10.0.0.2",
|
||||
username: "admin",
|
||||
port: 22,
|
||||
os: "linux",
|
||||
group: "",
|
||||
tags: [],
|
||||
protocol: "ssh",
|
||||
moshEnabled: true,
|
||||
telnetEnabled: true,
|
||||
telnetPort: 23,
|
||||
};
|
||||
|
||||
test("terminal updates preserve saved connection protocol and port", () => {
|
||||
const telnetSessionHost: Host = {
|
||||
...savedHost,
|
||||
protocol: "telnet",
|
||||
port: 23,
|
||||
moshEnabled: false,
|
||||
fontFamily: "jetbrains-mono",
|
||||
fontFamilyOverride: true,
|
||||
};
|
||||
|
||||
const merged = mergeTerminalHostUpdate(savedHost, telnetSessionHost);
|
||||
|
||||
assert.equal(merged.protocol, "ssh");
|
||||
assert.equal(merged.port, 22);
|
||||
assert.equal(merged.moshEnabled, true);
|
||||
assert.equal(merged.telnetEnabled, true);
|
||||
assert.equal(merged.telnetPort, 23);
|
||||
assert.equal(merged.fontFamily, "jetbrains-mono");
|
||||
assert.equal(merged.fontFamilyOverride, true);
|
||||
});
|
||||
|
||||
test("terminal updates still persist credentials entered during connection", () => {
|
||||
const credentialUpdate: Host = {
|
||||
...savedHost,
|
||||
protocol: "telnet",
|
||||
port: 23,
|
||||
moshEnabled: false,
|
||||
username: "deploy",
|
||||
authMethod: "password",
|
||||
password: "secret",
|
||||
};
|
||||
|
||||
const merged = mergeTerminalHostUpdate(savedHost, credentialUpdate);
|
||||
|
||||
assert.equal(merged.protocol, "ssh");
|
||||
assert.equal(merged.port, 22);
|
||||
assert.equal(merged.moshEnabled, true);
|
||||
assert.equal(merged.username, "deploy");
|
||||
assert.equal(merged.authMethod, "password");
|
||||
assert.equal(merged.password, "secret");
|
||||
});
|
||||
|
||||
test("terminal updates still persist SFTP bookmarks", () => {
|
||||
const bookmarkUpdate: Host = {
|
||||
...savedHost,
|
||||
protocol: "telnet",
|
||||
port: 23,
|
||||
moshEnabled: false,
|
||||
sftpBookmarks: [{ id: "bookmark-1", path: "/srv/www", label: "/srv/www" }],
|
||||
};
|
||||
|
||||
const merged = mergeTerminalHostUpdate(savedHost, bookmarkUpdate);
|
||||
|
||||
assert.equal(merged.protocol, "ssh");
|
||||
assert.equal(merged.port, 22);
|
||||
assert.equal(merged.moshEnabled, true);
|
||||
assert.deepEqual(merged.sftpBookmarks, [
|
||||
{ id: "bookmark-1", path: "/srv/www", label: "/srv/www" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("partial terminal updates preserve unrelated saved host fields", () => {
|
||||
const hostWithAppearance: Host = {
|
||||
...savedHost,
|
||||
fontSize: undefined,
|
||||
fontSizeOverride: false,
|
||||
showLineTimestamps: false,
|
||||
};
|
||||
|
||||
const merged = mergeTerminalHostUpdate(hostWithAppearance, {
|
||||
id: hostWithAppearance.id,
|
||||
showLineTimestamps: true,
|
||||
});
|
||||
|
||||
assert.equal(merged.showLineTimestamps, true);
|
||||
assert.equal(merged.fontSize, undefined);
|
||||
assert.equal(merged.fontSizeOverride, false);
|
||||
assert.equal(merged.hostname, hostWithAppearance.hostname);
|
||||
});
|
||||
|
||||
test("applySessionFontSizeToHost overlays workspace pane font size", () => {
|
||||
const host: Host = {
|
||||
id: "host-1",
|
||||
label: "Server",
|
||||
hostname: "example.com",
|
||||
username: "root",
|
||||
port: 22,
|
||||
group: "",
|
||||
tags: [],
|
||||
};
|
||||
const session = {
|
||||
id: "session-1",
|
||||
hostId: "host-1",
|
||||
hostLabel: "Server",
|
||||
username: "root",
|
||||
hostname: "example.com",
|
||||
status: "connected" as const,
|
||||
workspaceId: "workspace-1",
|
||||
fontSize: 18,
|
||||
fontSizeOverride: true,
|
||||
};
|
||||
|
||||
const merged = applySessionFontSizeToHost(host, session);
|
||||
|
||||
assert.equal(merged.fontSize, 18);
|
||||
assert.equal(merged.fontSizeOverride, true);
|
||||
});
|
||||
|
||||
test("terminal appearance reset clears only appearance fields", () => {
|
||||
const hostWithAppearance: Host = {
|
||||
...savedHost,
|
||||
fontSize: 16,
|
||||
fontSizeOverride: true,
|
||||
};
|
||||
const resetUpdate: Host = {
|
||||
...hostWithAppearance,
|
||||
protocol: "telnet",
|
||||
port: 23,
|
||||
moshEnabled: false,
|
||||
fontSize: undefined,
|
||||
fontSizeOverride: false,
|
||||
};
|
||||
|
||||
const merged = mergeTerminalHostUpdate(hostWithAppearance, resetUpdate);
|
||||
|
||||
assert.equal(merged.protocol, "ssh");
|
||||
assert.equal(merged.port, 22);
|
||||
assert.equal(merged.moshEnabled, true);
|
||||
assert.equal(merged.fontSize, undefined);
|
||||
assert.equal(merged.fontSizeOverride, false);
|
||||
});
|
||||
|
||||
test("follow-theme resolver: dark + auto follows the active dark UI preset", () => {
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"ui-midnight",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-theme resolver: light + auto follows the active light UI preset", () => {
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "light",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"ui-snow",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-theme resolver: imported system presets follow the active light or dark mode", () => {
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "light",
|
||||
lightUiThemeId: "github",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"system-github-light",
|
||||
);
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "github",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"system-github-dark",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-theme resolver: explicit dark override is ignored while following app theme", () => {
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"ui-midnight",
|
||||
);
|
||||
});
|
||||
|
||||
test("manual-theme resolver: explicit per-mode theme wins when not following app theme", () => {
|
||||
assert.equal(
|
||||
resolveManualTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
terminalThemeDarkId: "dracula",
|
||||
terminalThemeLightId: TERMINAL_THEME_AUTO,
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"dracula",
|
||||
);
|
||||
assert.equal(
|
||||
resolveManualTerminalThemeId({
|
||||
resolvedTheme: "light",
|
||||
terminalThemeDarkId: TERMINAL_THEME_AUTO,
|
||||
terminalThemeLightId: "solarized-light",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"solarized-light",
|
||||
);
|
||||
});
|
||||
|
||||
test("manual-theme resolver: auto preserves the saved global terminal theme", () => {
|
||||
assert.equal(
|
||||
resolveManualTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
terminalThemeDarkId: TERMINAL_THEME_AUTO,
|
||||
terminalThemeLightId: TERMINAL_THEME_AUTO,
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "midnight",
|
||||
fallbackThemeId: "dracula",
|
||||
}),
|
||||
"dracula",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-app sidebar pick resolves with the target mode before resolvedTheme catches up", () => {
|
||||
assert.equal(
|
||||
resolveFollowAppTerminalThemeId("system-flexoki-light", {
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "flexoki",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"system-flexoki-light",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-app pick settles once ui theme and resolved mode match the selection", () => {
|
||||
assert.equal(
|
||||
isFollowAppThemePickSettled("system-flexoki-light", {
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isFollowAppThemePickSettled("system-flexoki-light", {
|
||||
resolvedTheme: "light",
|
||||
lightUiThemeId: "snow",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isFollowAppThemePickSettled("system-flexoki-light", {
|
||||
resolvedTheme: "light",
|
||||
lightUiThemeId: "flexoki",
|
||||
darkUiThemeId: "github",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
getFollowAppThemePickExpectedSettledId("system-flexoki-light"),
|
||||
"system-flexoki-light",
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-app theme selection updates the matching mode and app theme", () => {
|
||||
assert.deepEqual(
|
||||
getFollowAppTerminalThemeSelectionUpdate("ui-snow"),
|
||||
{
|
||||
appTheme: "light",
|
||||
uiThemeId: "snow",
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
getFollowAppTerminalThemeSelectionUpdate("system-github-dark"),
|
||||
{
|
||||
appTheme: "dark",
|
||||
uiThemeId: "github",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("follow-app theme list only includes terminal themes backed by UI themes", () => {
|
||||
const lightIds = getFollowAppTerminalThemeIds("light");
|
||||
const darkIds = getFollowAppTerminalThemeIds("dark");
|
||||
|
||||
assert.ok(lightIds.includes("ui-snow"));
|
||||
assert.ok(lightIds.includes("system-github-light"));
|
||||
assert.ok(darkIds.includes("ui-midnight"));
|
||||
assert.ok(darkIds.includes("system-github-dark"));
|
||||
assert.equal(isFollowAppTerminalThemeId("dracula"), false);
|
||||
assert.equal(isFollowAppTerminalThemeId("custom-theme"), false);
|
||||
});
|
||||
|
||||
test("follow-theme resolver: auto with no UI match falls back to fallbackThemeId", () => {
|
||||
assert.equal(
|
||||
resolveFollowedTerminalThemeId({
|
||||
resolvedTheme: "dark",
|
||||
lightUiThemeId: "no-such-ui-theme",
|
||||
darkUiThemeId: "no-such-ui-theme",
|
||||
fallbackThemeId: "netcatty-dark",
|
||||
}),
|
||||
"netcatty-dark",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user