mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
chore: lock the plugin boundary and remove 2.9.0 leftovers
This commit is contained in:
1 parent
6489b62ece
commit
2961297989
399 files changed
+3964
-5627
No files matched your search
@@ -11,7 +11,8 @@ vi.mock("@/dashboard/dashboard-cards-registry", () => ({
|
||||
getRegisteredDashboardCard: (id: string) => registered.cards.get(id),
|
||||
}));
|
||||
|
||||
const { buildDashboardSlots } = await import("../../lib/apply-ui-preset.js");
|
||||
const { buildDashboardSlots, presetHiddenRailTabs } =
|
||||
await import("../../lib/apply-ui-preset.js");
|
||||
|
||||
afterEach(() => {
|
||||
registered.cards.clear();
|
||||
@@ -69,3 +70,24 @@ describe("buildDashboardSlots", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("presetHiddenRailTabs", () => {
|
||||
const items = [
|
||||
{ id: "snippets", simplePreset: true },
|
||||
{ id: "docker-like" },
|
||||
{ id: "pinned", hideable: false },
|
||||
];
|
||||
|
||||
it("hides plugin items that do not opt in when the preset asks", () => {
|
||||
expect(
|
||||
presetHiddenRailTabs(
|
||||
{ hiddenTabs: ["macros"], hidePluginItems: true },
|
||||
items,
|
||||
),
|
||||
).toEqual(["macros", "docker-like"]);
|
||||
});
|
||||
|
||||
it("leaves plugin items alone otherwise", () => {
|
||||
expect(presetHiddenRailTabs({ hiddenTabs: [] }, items)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createKeyedRequestCache } from "../../lib/keyed-request-cache";
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
describe("createKeyedRequestCache", () => {
|
||||
it("coalesces requests per key without mixing keys", async () => {
|
||||
let resolve!: (value: string) => void;
|
||||
const loader = vi.fn(() => new Promise<string>((done) => (resolve = done)));
|
||||
const cache = createKeyedRequestCache<string>(5_000);
|
||||
|
||||
const first = cache.get("host:1", loader);
|
||||
const duplicate = cache.get("host:1", loader);
|
||||
const other = cache.get("host:2", async () => "other");
|
||||
resolve("shared");
|
||||
|
||||
await expect(Promise.all([first, duplicate, other])).resolves.toEqual([
|
||||
"shared",
|
||||
"shared",
|
||||
"other",
|
||||
]);
|
||||
expect(loader).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("can show stale data immediately while forcing one refresh", async () => {
|
||||
vi.useFakeTimers();
|
||||
const cache = createKeyedRequestCache<string>(1_000);
|
||||
await cache.get("directory", async () => "old");
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
|
||||
expect(cache.peek("directory", 30_000)).toBe("old");
|
||||
|
||||
const loader = vi.fn(async () => "fresh");
|
||||
const [a, b] = await Promise.all([
|
||||
cache.get("directory", loader, { force: true }),
|
||||
cache.get("directory", loader, { force: true }),
|
||||
]);
|
||||
expect([a, b]).toEqual(["fresh", "fresh"]);
|
||||
expect(loader).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("evicts the least recently used entry when bounded", async () => {
|
||||
const cache = createKeyedRequestCache<number>(10_000, 2);
|
||||
await cache.get("a", async () => 1);
|
||||
await cache.get("b", async () => 2);
|
||||
expect(cache.peek("a")).toBe(1);
|
||||
await cache.get("c", async () => 3);
|
||||
|
||||
expect(cache.peek("a")).toBe(1);
|
||||
expect(cache.peek("b")).toBeNull();
|
||||
expect(cache.peek("c")).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps stale data when a refresh fails", async () => {
|
||||
const cache = createKeyedRequestCache<string>(1_000);
|
||||
await cache.get("key", async () => "safe");
|
||||
|
||||
await expect(
|
||||
cache.get(
|
||||
"key",
|
||||
async () => {
|
||||
throw new Error("offline");
|
||||
},
|
||||
{ force: true },
|
||||
),
|
||||
).rejects.toThrow("offline");
|
||||
expect(cache.peek("key")).toBe("safe");
|
||||
});
|
||||
|
||||
it("does not let an invalidated request overwrite newer data", async () => {
|
||||
let resolveOld!: (value: string) => void;
|
||||
const cache = createKeyedRequestCache<string>(10_000);
|
||||
const oldRequest = cache.get(
|
||||
"key",
|
||||
() => new Promise<string>((resolve) => (resolveOld = resolve)),
|
||||
);
|
||||
|
||||
cache.set("key", "optimistic");
|
||||
resolveOld("old");
|
||||
await oldRequest;
|
||||
|
||||
expect(cache.peek("key")).toBe("optimistic");
|
||||
});
|
||||
});
|
||||
@@ -10,10 +10,6 @@ const api = vi.hoisted(() => ({
|
||||
adminDeleteUserHost: vi.fn(async () => ({})),
|
||||
adminGetUserCredentials: vi.fn(async () => [] as unknown[]),
|
||||
adminDeleteUserCredential: vi.fn(async () => ({})),
|
||||
adminGetUserSnippets: vi.fn(async () => [] as unknown[]),
|
||||
adminCreateUserSnippet: vi.fn(async () => ({})),
|
||||
adminUpdateUserSnippet: vi.fn(async () => ({})),
|
||||
adminDeleteUserSnippet: vi.fn(async () => ({})),
|
||||
adminResetUserPassword: vi.fn(async () => ({ dataWiped: false })),
|
||||
adminExportUserData: vi.fn(async () => ({})),
|
||||
getSessions: vi.fn(async () => ({ sessions: [] as unknown[] })),
|
||||
@@ -87,7 +83,6 @@ describe("AdminUserManagePanel", () => {
|
||||
"admin.manageTabAccount",
|
||||
"admin.manageTabHosts",
|
||||
"admin.manageTabCredentials",
|
||||
"admin.manageTabSnippets",
|
||||
"admin.manageTabSessions",
|
||||
"admin.manageTabDanger",
|
||||
]) {
|
||||
@@ -97,7 +92,6 @@ describe("AdminUserManagePanel", () => {
|
||||
await waitFor(() => {
|
||||
expect(api.adminGetUserHosts).toHaveBeenCalledWith("u2");
|
||||
expect(api.adminGetUserCredentials).toHaveBeenCalledWith("u2");
|
||||
expect(api.adminGetUserSnippets).toHaveBeenCalledWith("u2");
|
||||
expect(api.getUserRoles).toHaveBeenCalledWith("u2");
|
||||
});
|
||||
});
|
||||
@@ -113,7 +107,6 @@ describe("AdminUserManagePanel", () => {
|
||||
|
||||
expect(api.adminGetUserHosts).not.toHaveBeenCalled();
|
||||
expect(api.adminGetUserCredentials).not.toHaveBeenCalled();
|
||||
expect(api.adminGetUserSnippets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lists the user's hosts and opens a tab via the connect button", async () => {
|
||||
@@ -142,28 +135,43 @@ describe("AdminUserManagePanel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a snippet for the target user", async () => {
|
||||
renderPanel(makeUser());
|
||||
|
||||
await userEvent.click(screen.getByText("admin.manageTabSnippets"));
|
||||
await userEvent.click(screen.getByText("admin.addSnippetForUser"));
|
||||
await userEvent.type(
|
||||
screen.getByPlaceholderText("admin.snippetNamePlaceholder"),
|
||||
"restart",
|
||||
);
|
||||
await userEvent.type(
|
||||
screen.getByPlaceholderText("admin.snippetContentPlaceholder"),
|
||||
"systemctl restart nginx",
|
||||
);
|
||||
await userEvent.click(screen.getByText("common.save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.adminCreateUserSnippet).toHaveBeenCalledWith("u2", {
|
||||
name: "restart",
|
||||
content: "systemctl restart nginx",
|
||||
folder: null,
|
||||
});
|
||||
it("shows a tab a plugin contributes, with the user it manages", async () => {
|
||||
const { registerSlotContribution } =
|
||||
await import("../../shell/action-registry");
|
||||
const dispose = registerSlotContribution("admin.userTabs", {
|
||||
actionId: "fixture.adminTab",
|
||||
titleKey: "fixture.tabTitle",
|
||||
kind: "component",
|
||||
component: ((props: { user: AdminUser }) => (
|
||||
<div>managing {props.user.username}</div>
|
||||
)) as never,
|
||||
});
|
||||
try {
|
||||
renderPanel(makeUser());
|
||||
await userEvent.click(screen.getByText("fixture.tabTitle"));
|
||||
expect(screen.getByText("managing bob")).toBeTruthy();
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows the locked notice instead of a plugin tab while data is locked", async () => {
|
||||
const { registerSlotContribution } =
|
||||
await import("../../shell/action-registry");
|
||||
const dispose = registerSlotContribution("admin.userTabs", {
|
||||
actionId: "fixture.adminTab",
|
||||
titleKey: "fixture.tabTitle",
|
||||
kind: "component",
|
||||
component: (() => <div>plugin content</div>) as never,
|
||||
});
|
||||
try {
|
||||
renderPanel(makeUser({ dataUnlocked: false }));
|
||||
await userEvent.click(screen.getByText("fixture.tabTitle"));
|
||||
expect(screen.queryByText("plugin content")).toBeNull();
|
||||
expect(screen.getByText("admin.dataLockedNotice")).toBeTruthy();
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("deletes the user from the danger tab after confirmation", async () => {
|
||||
|
||||
@@ -97,4 +97,19 @@ describe("createQuickConnectHost for remote desktop protocols", () => {
|
||||
"demo-desktopPassword": "pw",
|
||||
});
|
||||
});
|
||||
|
||||
it("carries the fields a plugin's SSH auth editor filled in", () => {
|
||||
const host = createQuickConnectHost({
|
||||
ip: "100.64.0.9",
|
||||
port: 22,
|
||||
username: "root",
|
||||
authType: "tailnet-login",
|
||||
authFields: { pluginSettings: { demo: { profileId: 3 } } },
|
||||
});
|
||||
expect(host.authType).toBe("tailnet-login");
|
||||
expect(host.password).toBeUndefined();
|
||||
expect(host).toMatchObject({
|
||||
pluginSettings: { demo: { profileId: 3 } },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -94,20 +94,18 @@ describe("sanitizeUiOverrides", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("drops the retired homepage area", () => {
|
||||
expect(sanitizeUiOverrides({ homepage: { enabledWidgets: null } })).toEqual(
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("filters non-strings out of string arrays instead of rejecting them", () => {
|
||||
const result = sanitizeUiOverrides({
|
||||
rail: { hiddenTabs: ["serial", 7, null, "history"] },
|
||||
});
|
||||
expect(result.rail?.hiddenTabs).toEqual(["serial", "history"]);
|
||||
});
|
||||
|
||||
it("keeps an explicit null for nullable array knobs", () => {
|
||||
expect(sanitizeUiOverrides({ homepage: { enabledWidgets: null } })).toEqual(
|
||||
{
|
||||
homepage: { enabledWidgets: null },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveArea", () => {
|
||||
@@ -155,12 +153,13 @@ describe("PRESETS.balanced", () => {
|
||||
expect(PRESETS.balanced.terminal.toolbarDensity).toBe("labeled");
|
||||
expect(PRESETS.balanced.fileManager.viewMode).toBe("grid");
|
||||
expect(PRESETS.balanced.hostEditor.mode).toBe("full");
|
||||
expect(PRESETS.balanced.homepage.enabledWidgets).toBeNull();
|
||||
});
|
||||
|
||||
it("never lets a preset drive the homepage canvas", () => {
|
||||
expect(PRESETS.simple.homepage.enabledWidgets).toBeNull();
|
||||
expect(PRESETS.advanced.homepage.enabledWidgets).toBeNull();
|
||||
it("keeps core's basics in Simple and hides plugin rail items that do not opt in", () => {
|
||||
expect(PRESETS.simple.rail.hiddenTabs).not.toContain("hosts");
|
||||
expect(PRESETS.simple.rail.hiddenTabs).not.toContain("connections");
|
||||
expect(PRESETS.simple.rail.hidePluginItems).toBe(true);
|
||||
expect(PRESETS.balanced.rail.hidePluginItems).toBeFalsy();
|
||||
});
|
||||
|
||||
it("leaves the wide dashboard cards off by default in every preset", () => {
|
||||
|
||||
Reference in new issue
Block a user