mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
264 lines
8.4 KiB
TypeScript
264 lines
8.4 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { Boxes } from "lucide-react";
|
|
import {
|
|
hideableRailIds,
|
|
permittedRailItems,
|
|
promotableIds,
|
|
RAIL_ITEMS,
|
|
RAIL_UTILITY_ITEMS,
|
|
registerRailItem,
|
|
resetRegisteredRailItems,
|
|
rightDockableIds,
|
|
railItemLabel,
|
|
visibleRailItems,
|
|
} from "@/sidebar/rail-items";
|
|
import { isCapturableTabType } from "@/shell/shell-layout";
|
|
import en from "@/locales/en.json";
|
|
|
|
afterEach(() => resetRegisteredRailItems());
|
|
|
|
function lookup(key: string): unknown {
|
|
return key
|
|
.split(".")
|
|
.reduce<unknown>(
|
|
(acc, part) =>
|
|
acc && typeof acc === "object"
|
|
? (acc as Record<string, unknown>)[part]
|
|
: undefined,
|
|
en,
|
|
);
|
|
}
|
|
|
|
describe("RAIL_ITEMS", () => {
|
|
it("has no duplicate ids", () => {
|
|
const ids = [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS].map((item) => item.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
it("gives every destination a real translation key", () => {
|
|
for (const item of [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]) {
|
|
expect(
|
|
typeof lookup(item.labelKey),
|
|
`missing translation for ${item.id} (${item.labelKey})`,
|
|
).toBe("string");
|
|
}
|
|
});
|
|
|
|
it("keeps the destinations that shipped before the lists were merged", () => {
|
|
// Guards against a destination silently disappearing now that the rail,
|
|
// the settings toggles, the sidebar titles and the mobile bar all read
|
|
// from this one list.
|
|
// "sftp" moved to the file-manager plugin's own registerRailItem,
|
|
// "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.
|
|
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
|
|
"hosts",
|
|
"credentials",
|
|
"connections",
|
|
"quick-connect",
|
|
"ssh-tools",
|
|
"macros",
|
|
"split-screen",
|
|
]);
|
|
});
|
|
|
|
it("exposes every visible rail item as hideable", () => {
|
|
expect(hideableRailIds()).toEqual(
|
|
visibleRailItems().map((item) => item.id),
|
|
);
|
|
});
|
|
|
|
it("marks exactly the three mobile primary slots", () => {
|
|
expect(
|
|
RAIL_ITEMS.filter((item) => item.mobilePrimary).map((item) => item.id),
|
|
).toEqual(["hosts", "quick-connect", "ssh-tools"]);
|
|
});
|
|
|
|
it("marks the panels that can open as a tab", () => {
|
|
expect(
|
|
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
|
.filter((item) => item.promotable)
|
|
.map((item) => item.id),
|
|
).toEqual(["ssh-tools", "macros"]);
|
|
});
|
|
|
|
it("derives promotableIds from the promotable flag", () => {
|
|
// The header button and the hint both gate on this list, so a drift here
|
|
// silently hides the feature for that panel.
|
|
expect(promotableIds()).toEqual(
|
|
[...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]
|
|
.filter((item) => item.promotable)
|
|
.map((item) => item.id),
|
|
);
|
|
});
|
|
|
|
it("every promotable id is also a captured workspace tab type", () => {
|
|
// The id doubles as the TabType, so a promoted panel that isn't capturable
|
|
// would silently vanish from saved workspaces.
|
|
for (const item of [...RAIL_ITEMS, ...RAIL_UTILITY_ITEMS]) {
|
|
if (!item.promotable) continue;
|
|
expect(
|
|
isCapturableTabType(item.id),
|
|
`${item.id} is promotable but not workspace-capturable`,
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps the mounted-but-hidden panels out of the right dock", () => {
|
|
// Hosts, credentials and fleets stay mounted while hidden and share editing
|
|
// state, so a second live instance in the right dock would fight the first.
|
|
for (const id of ["hosts", "credentials"]) {
|
|
expect(
|
|
rightDockableIds(),
|
|
`${id} must not be right-dockable`,
|
|
).not.toContain(id);
|
|
}
|
|
});
|
|
|
|
it("only offers reference panels in the right dock", () => {
|
|
expect(rightDockableIds()).toEqual(["connections", "ssh-tools", "macros"]);
|
|
});
|
|
});
|
|
|
|
describe("registered rail items", () => {
|
|
const item = (id: string, extra: object = {}) => ({
|
|
id,
|
|
icon: Boxes,
|
|
labelKey: `nav.${id}`,
|
|
pluginId: "p",
|
|
...extra,
|
|
});
|
|
|
|
it("places items after their anchor, keeping their own order", () => {
|
|
registerRailItem(item("second", { after: "macros", order: 2 }));
|
|
registerRailItem(item("first", { after: "macros", order: 1 }));
|
|
const ids = visibleRailItems().map((entry) => entry.id);
|
|
const at = ids.indexOf("macros");
|
|
expect(ids.slice(at + 1, at + 3)).toEqual(["first", "second"]);
|
|
});
|
|
|
|
it("goes to the end when its anchor does not exist", () => {
|
|
registerRailItem(item("orphan", { after: "nowhere" }));
|
|
expect(visibleRailItems().at(-1)?.id).toBe("orphan");
|
|
});
|
|
|
|
it("feeds its flags into the promotable, dock and hideable lists", () => {
|
|
registerRailItem(item("docked", { promotable: true, rightDockable: true }));
|
|
registerRailItem(item("pinned", { hideable: false }));
|
|
expect(promotableIds()).toContain("docked");
|
|
expect(rightDockableIds()).toContain("docked");
|
|
expect(hideableRailIds()).toContain("docked");
|
|
expect(hideableRailIds()).not.toContain("pinned");
|
|
});
|
|
|
|
it("drops a hidden item everywhere without unregistering it", () => {
|
|
registerRailItem(item("off", { hidden: true, promotable: true }));
|
|
expect(visibleRailItems().map((entry) => entry.id)).not.toContain("off");
|
|
expect(hideableRailIds()).not.toContain("off");
|
|
expect(promotableIds()).not.toContain("off");
|
|
});
|
|
|
|
it("labels a registered item by its own key", () => {
|
|
registerRailItem(item("labelled"));
|
|
expect(railItemLabel("labelled", (key) => `t:${key}`)).toBe(
|
|
"t:nav.labelled",
|
|
);
|
|
});
|
|
|
|
it("disposes only the entry it registered", () => {
|
|
const dispose = registerRailItem(item("same", { order: 1 }));
|
|
registerRailItem(item("same", { order: 2 }));
|
|
dispose();
|
|
expect(visibleRailItems().map((entry) => entry.id)).toContain("same");
|
|
});
|
|
});
|
|
|
|
describe("railItemLabel", () => {
|
|
it("translates known destinations", () => {
|
|
expect(railItemLabel("hosts", () => "Hosts")).toBe("Hosts");
|
|
});
|
|
|
|
it("falls back to the id for anything unknown", () => {
|
|
expect(railItemLabel("not-a-view", (k) => k)).toBe("not-a-view");
|
|
});
|
|
|
|
describe("electron-only items", () => {
|
|
afterEach(() => {
|
|
delete (window as { IS_ELECTRON?: boolean }).IS_ELECTRON;
|
|
});
|
|
|
|
const desktopOnly = {
|
|
id: "desktop-only",
|
|
icon: Boxes,
|
|
labelKey: "nav.desktopOnly",
|
|
pluginId: "p",
|
|
electronOnly: true,
|
|
};
|
|
|
|
it("hides electron-only destinations in the browser build", () => {
|
|
const dispose = registerRailItem(desktopOnly);
|
|
const ids = visibleRailItems().map((item) => item.id);
|
|
dispose();
|
|
expect(ids).not.toContain("desktop-only");
|
|
});
|
|
|
|
it("shows electron-only destinations in the desktop app", () => {
|
|
(window as { IS_ELECTRON?: boolean }).IS_ELECTRON = true;
|
|
const dispose = registerRailItem(desktopOnly);
|
|
const ids = visibleRailItems().map((item) => item.id);
|
|
dispose();
|
|
expect(ids).toContain("desktop-only");
|
|
});
|
|
|
|
it("keeps every non-electron item in both builds", () => {
|
|
const browser = visibleRailItems().map((item) => item.id);
|
|
(window as { IS_ELECTRON?: boolean }).IS_ELECTRON = true;
|
|
const desktop = visibleRailItems().map((item) => item.id);
|
|
const electronOnly = RAIL_ITEMS.filter((item) => item.electronOnly).map(
|
|
(item) => item.id,
|
|
);
|
|
expect(desktop.filter((id) => !electronOnly.includes(id))).toEqual(
|
|
browser,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("permittedRailItems", () => {
|
|
const items = [
|
|
{ id: "open", icon: Boxes, labelKey: "nav.hosts" },
|
|
{
|
|
id: "gated",
|
|
icon: Boxes,
|
|
labelKey: "nav.hosts",
|
|
permission: "demo.use",
|
|
},
|
|
];
|
|
|
|
it("hides an item gated on a permission the user lacks", () => {
|
|
const ids = permittedRailItems(items, {
|
|
loaded: true,
|
|
has: () => false,
|
|
}).map((item) => item.id);
|
|
expect(ids).toEqual(["open"]);
|
|
});
|
|
|
|
it("shows it once the user holds the permission", () => {
|
|
const ids = permittedRailItems(items, {
|
|
loaded: true,
|
|
has: (permission) => permission === "demo.use",
|
|
}).map((item) => item.id);
|
|
expect(ids).toEqual(["open", "gated"]);
|
|
});
|
|
|
|
it("keeps gated items hidden until permissions have loaded", () => {
|
|
const ids = permittedRailItems(items, {
|
|
loaded: false,
|
|
has: () => true,
|
|
}).map((item) => item.id);
|
|
expect(ids).toEqual(["open"]);
|
|
});
|
|
});
|