* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 16:03:29 -05:00
1 parent aff29a9581
commit a89ccc6b07
577 files changed
+76479 -101831

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) =>
options && typeof options === "object" && !("defaultValue" in options)
? `${key}:${JSON.stringify(options)}`
: ((options?.defaultValue as string | undefined) ?? key),
i18n: { language: "en" },
}),
initReactI18next: { type: "3rdParty", init: () => {} },
}));
const permissions = vi.hoisted(() => ({ isAdmin: true }));
vi.mock("@/hooks/use-permissions", () => ({
usePermissions: () => ({
isAdmin: permissions.isAdmin,
permissions: [],
loaded: true,
has: () => permissions.isAdmin,
}),
}));
vi.mock("@/shell/controller/ShellControllerProvider", () => ({
ShellConfirmCapture: () => null,
}));
const plugins = vi.hoisted(() => ({
restoreBundledPlugin: vi.fn(async () => {}),
}));
vi.mock("@/api/plugins-api", () => plugins);
vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
const look = vi.hoisted(() => ({
chooseLayout: vi.fn(async () => {}),
chooseThemeForAccount: vi.fn(async () => {}),
chooseDefaultLook: vi.fn(async () => {}),
chooseTheme: vi.fn(),
}));
vi.mock("@/look/look-store", () => ({
chooseLayout: look.chooseLayout,
chooseThemeForAccount: look.chooseThemeForAccount,
chooseDefaultLook: look.chooseDefaultLook,
}));
vi.mock("@/look/use-look-theme", () => ({
useChooseTheme: () => look.chooseTheme,
}));
const manager = vi.hoisted(() => ({
requestUpdate: vi.fn(),
requestInstall: vi.fn(),
toggle: vi.fn(),
setConsent: vi.fn(),
confirmConsent: vi.fn(),
}));
vi.mock("@/plugins/use-plugins-manager", () => ({
usePluginsManager: () => ({
...manager,
loading: false,
registryError: null,
consent: null,
busy: new Set<string>(),
plugins: [
{
id: "fancy-layout",
name: "Fancy",
installed: true,
enabled: true,
updateAvailable: true,
latestVersion: "1.1.0",
managedByServer: false,
bundled: true,
versions: [],
},
{
id: "paper-themes",
name: "Paper themes",
description: "Light themes",
installed: false,
enabled: false,
updateAvailable: false,
latestVersion: "1.0.0",
versions: [{ version: "1.0.0", capabilities: ["ui:theme"] }],
},
{
id: "docker",
name: "Docker",
installed: false,
versions: [{ version: "1.0.0", capabilities: ["ui:surface"] }],
},
],
}),
}));
import { RepairView } from "@/look/RepairView";
import { setPluginLayouts } from "@/look/layout-registry";
const status = {
kind: "repair" as const,
reason: "failed" as const,
layoutId: "fancy",
pluginId: "fancy-layout",
version: "1.0.0",
error: "activate threw",
};
const onRetry = vi.fn();
beforeEach(() => {
permissions.isAdmin = true;
for (const fn of [
...Object.values(look),
...Object.values(manager),
plugins.restoreBundledPlugin,
onRetry,
]) {
fn.mockClear();
}
setPluginLayouts([
{ id: "fancy", label: "Fancy", pluginId: "fancy-layout" },
{ id: "classic", label: "Classic", pluginId: "classic-layout" },
]);
});
afterEach(cleanup);
describe("RepairView", () => {
it("names the layout and the error, and copies the details", async () => {
const writeText = vi.fn(async () => {});
Object.assign(navigator, { clipboard: { writeText } });
render(<RepairView status={status} onRetry={onRetry} />);
expect(screen.getByText("activate threw")).toBeTruthy();
expect(screen.getByText("looks.repair.reasons.failed")).toBeTruthy();
fireEvent.click(screen.getByText("looks.repair.copy"));
await waitFor(() => expect(writeText).toHaveBeenCalled());
const copied = String((writeText.mock.calls[0] as unknown[])[0]);
expect(copied).toContain("fancy");
expect(copied).toContain("activate threw");
});
it("retries", () => {
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(screen.getByText("looks.repair.retry"));
expect(onRetry).toHaveBeenCalled();
});
it("switches to another layout or theme", () => {
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(screen.getByText("Classic"));
expect(look.chooseLayout).toHaveBeenCalledWith("classic");
fireEvent.click(
screen.getAllByTitle("newUi.sidebar.userProfile.themeLight")[0],
);
expect(look.chooseTheme).toHaveBeenCalledWith("light");
});
it("resets to the default look", async () => {
localStorage.setItem("vite-ui-theme", "dracula");
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(screen.getByText("looks.repair.reset"));
await waitFor(() => expect(look.chooseLayout).toHaveBeenCalledWith(null));
expect(look.chooseThemeForAccount).toHaveBeenCalledWith(null);
expect(localStorage.getItem("vite-ui-theme")).toBeNull();
fireEvent.click(screen.getByText("looks.repair.resetEveryone"));
expect(look.chooseDefaultLook).toHaveBeenCalledWith({
layout: null,
theme: null,
});
});
it("lets an admin update or turn off the plugin", () => {
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(
screen.getByText(
'looks.repair.update:{"name":"Fancy","version":"1.1.0"}',
),
);
expect(manager.requestUpdate).toHaveBeenCalledWith(
expect.objectContaining({ id: "fancy-layout" }),
);
fireEvent.click(screen.getByText('looks.repair.disable:{"name":"Fancy"}'));
expect(manager.toggle).toHaveBeenCalledWith(
expect.objectContaining({ id: "fancy-layout" }),
);
});
it("lets an admin restore the bundled copy and then tries again", async () => {
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(screen.getByText('looks.repair.restore:{"name":"Fancy"}'));
expect(plugins.restoreBundledPlugin).toHaveBeenCalledWith("fancy-layout");
await waitFor(() => expect(onRetry).toHaveBeenCalled());
});
it("names the missing layout when none is set", () => {
render(
<RepairView
status={{
kind: "repair",
reason: "missing",
layoutId: "",
}}
onRetry={onRetry}
/>,
);
expect(screen.getByText("looks.repair.noLayout")).toBeTruthy();
});
it("lets an admin install a layout or theme from the registries", () => {
render(<RepairView status={status} onRetry={onRetry} />);
fireEvent.click(screen.getByText("looks.repair.install"));
expect(screen.getByText("Paper themes")).toBeTruthy();
expect(screen.queryByText("Docker")).toBeNull();
fireEvent.click(screen.getByText("looks.repair.installButton"));
expect(manager.requestInstall).toHaveBeenCalledWith(
expect.objectContaining({ id: "paper-themes" }),
);
});
it("gives everyone else switch and reset, and a note to ask an admin", () => {
permissions.isAdmin = false;
render(<RepairView status={status} onRetry={onRetry} />);
expect(screen.getByText("looks.repair.askAdmin")).toBeTruthy();
expect(screen.getByText("looks.repair.reset")).toBeTruthy();
expect(screen.getByText("looks.repair.switchLayout")).toBeTruthy();
expect(screen.queryByText("looks.repair.resetEveryone")).toBeNull();
expect(screen.queryByText("looks.repair.install")).toBeNull();
expect(screen.queryByText(/looks\.repair\.update/)).toBeNull();
expect(screen.queryByText(/looks\.repair\.disable/)).toBeNull();
});
it("offers no retry in safe mode", () => {
render(
<RepairView
status={{ ...status, reason: "safe-url" }}
onRetry={onRetry}
/>,
);
expect(screen.getByText("looks.repair.safeTitle")).toBeTruthy();
expect(screen.queryByText("looks.repair.retry")).toBeNull();
});
});