mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: move snippets, keybinding actions and macros out of core into plugins
This commit is contained in:
1 parent
d9bba10612
commit
e0c30a4e6f
262 files changed
+9091
-10744
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { act, fireEvent, screen } from "@testing-library/react";
|
||||
import { runKeybindingAction } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
renderWithApp,
|
||||
type RenderedPluginApp,
|
||||
@@ -41,11 +42,24 @@ describe(`${manifest.id} activate`, () => {
|
||||
expect(railItem?.permission).toBe("snippets.view");
|
||||
});
|
||||
|
||||
it("registers the snippets.resolveForTerminal action", async () => {
|
||||
it("registers what other surfaces reach it through", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
expect(rendered.registered.actions()).toContain(
|
||||
"snippets.resolveForTerminal",
|
||||
expect(rendered.registered.actions()).toEqual(
|
||||
expect.arrayContaining([
|
||||
"snippets.startupCommand",
|
||||
"snippets.list",
|
||||
"snippets.extractInputs",
|
||||
]),
|
||||
);
|
||||
expect(rendered.registered.paletteGroups()).toEqual(["snippets"]);
|
||||
expect(rendered.registered.keybindingActions()).toEqual(["runSnippet"]);
|
||||
expect(rendered.registered.hostEditorSections()).toEqual(["snippets"]);
|
||||
expect(rendered.registered.slot("host-metrics.toolbar")).toEqual([
|
||||
"snippets.quickActions",
|
||||
]);
|
||||
expect(rendered.registered.slot("shell.overlay")).toEqual([
|
||||
"snippets.overlay",
|
||||
]);
|
||||
});
|
||||
|
||||
it("removes everything it registered on deactivate", async () => {
|
||||
@@ -54,6 +68,143 @@ describe(`${manifest.id} activate`, () => {
|
||||
expect(app.registered.panels()).toEqual([]);
|
||||
expect(app.registered.railItems()).toEqual([]);
|
||||
expect(app.registered.actions()).toEqual([]);
|
||||
expect(app.registered.paletteGroups()).toEqual([]);
|
||||
expect(app.registered.keybindingActions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const deploy = {
|
||||
id: 3,
|
||||
name: "Deploy",
|
||||
content: "deploy $HOST",
|
||||
description: null,
|
||||
isNote: false,
|
||||
};
|
||||
|
||||
describe("snippets in the command palette", () => {
|
||||
it("lists snippets that run in the target tab", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
permissions: ["snippets.view"],
|
||||
});
|
||||
const items = await rendered.loadPaletteGroup("snippets");
|
||||
expect(items).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "3",
|
||||
title: "Deploy",
|
||||
description: "deploy $HOST",
|
||||
needsTarget: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers nothing without the view permission", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
permissions: [],
|
||||
});
|
||||
expect(await rendered.loadPaletteGroup("snippets")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the runSnippet keybinding", () => {
|
||||
it("types the resolved snippet into the session it was pressed in", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: deploy })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
const send = vi.fn();
|
||||
const handled = runKeybindingAction(
|
||||
{ type: "runSnippet", snippetId: "3", appendEnter: true },
|
||||
{ host: { ip: "10.0.0.9" }, send },
|
||||
);
|
||||
expect(handled).toBe(true);
|
||||
await vi.waitFor(() =>
|
||||
expect(send).toHaveBeenCalledWith("deploy 10.0.0.9\r"),
|
||||
);
|
||||
expect(api.get).toHaveBeenCalledWith("/3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the startup command", () => {
|
||||
it("resolves the host's startup snippet, or nothing without one", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: deploy })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
const host = {
|
||||
id: "1",
|
||||
name: "web",
|
||||
ip: "10.0.0.2",
|
||||
port: 22,
|
||||
pluginSettings: { snippets: { startupSnippetId: 3 } },
|
||||
};
|
||||
expect(
|
||||
await rendered.app.invokeAction("snippets.startupCommand", host),
|
||||
).toBe("deploy 10.0.0.2");
|
||||
expect(
|
||||
await rendered.app.invokeAction("snippets.startupCommand", {
|
||||
...host,
|
||||
pluginSettings: {},
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the host editor's Snippets tab", () => {
|
||||
it("writes the startup snippet and quick actions into this plugin's settings", async () => {
|
||||
const api = { get: vi.fn(async () => ({ data: [deploy] })) };
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: api as never,
|
||||
});
|
||||
let form: Record<string, unknown> = {
|
||||
pluginSettings: { other: { keep: 1 } },
|
||||
};
|
||||
const updateForm = (
|
||||
patch: (current: Record<string, unknown>) => Record<string, unknown>,
|
||||
) => {
|
||||
form = patch(form);
|
||||
};
|
||||
rendered.renderHostEditorSection("snippets", { form, updateForm });
|
||||
const startup = await screen.findByRole("combobox", {
|
||||
name: "Startup Snippet",
|
||||
});
|
||||
await screen.findAllByRole("option", { name: "Deploy" });
|
||||
act(() => {
|
||||
fireEvent.change(startup, { target: { value: "3" } });
|
||||
});
|
||||
expect(form.pluginSettings).toEqual({
|
||||
other: { keep: 1 },
|
||||
snippets: { startupSnippetId: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the Host Metrics quick action buttons", () => {
|
||||
it("draws a button per quick action and nothing without any", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
permissions: ["snippets.view"],
|
||||
});
|
||||
const host = {
|
||||
id: 4,
|
||||
pluginSettings: {
|
||||
snippets: { quickActions: [{ name: "Restart", snippetId: 3 }] },
|
||||
},
|
||||
};
|
||||
rendered.renderSlot("host-metrics.toolbar", { hostId: 4, host });
|
||||
expect(await screen.findByRole("button", { name: "Restart" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,22 +31,23 @@ afterEach(async () => {
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
describe("snippet.list", () => {
|
||||
it("lists the user's own snippets", async () => {
|
||||
describe("snippet pickers while an admin edits another user's host", () => {
|
||||
it("list the user's own snippets", async () => {
|
||||
const api = fakeApi([{ id: 1, name: "a", content: "ls", isNote: true }]);
|
||||
rendered = await renderWithApp(plugin, { manifest, locales, api });
|
||||
const rows = await rendered.app.invokeAction("snippet.list");
|
||||
rendered.renderHostEditorSection("snippets", { form: {} });
|
||||
await screen.findAllByRole("option", { name: "a" });
|
||||
expect(api.get).toHaveBeenCalledWith("/");
|
||||
expect(rows).toEqual([
|
||||
{ id: 1, name: "a", content: "ls", folder: null, isNote: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists another user's snippets for an admin", async () => {
|
||||
it("list the target user's snippets", async () => {
|
||||
const api = fakeApi([]);
|
||||
rendered = await renderWithApp(plugin, { manifest, locales, api });
|
||||
await rendered.app.invokeAction("snippet.list", { targetUserId: "u2" });
|
||||
expect(api.get).toHaveBeenCalledWith("/", TARGET);
|
||||
rendered.renderHostEditorSection("snippets", {
|
||||
form: {},
|
||||
adminTargetUserId: "u2",
|
||||
});
|
||||
await waitFor(() => expect(api.get).toHaveBeenCalledWith("/", TARGET));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
extractSnippetInputs,
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
} from "../../src/frontend/snippet-variables";
|
||||
|
||||
describe("extractSnippetInputs", () => {
|
||||
it("extracts a single unlabeled input", () => {
|
||||
expect(extractSnippetInputs("ping $INPUT_1")).toEqual([
|
||||
{ key: "INPUT_1", label: "Input 1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("extracts a labeled input using brace syntax", () => {
|
||||
expect(
|
||||
extractSnippetInputs("nc -zv $HOST ${INPUT_1:Port to check}"),
|
||||
).toEqual([{ key: "INPUT_1", label: "Port to check" }]);
|
||||
});
|
||||
|
||||
it("plain (non-braced) references have no label", () => {
|
||||
expect(extractSnippetInputs("echo $INPUT_2")).toEqual([
|
||||
{ key: "INPUT_2", label: "Input 2" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("de-duplicates repeated references and preserves first-seen order", () => {
|
||||
expect(
|
||||
extractSnippetInputs("$INPUT_2 then $INPUT_1 then $INPUT_2 again"),
|
||||
).toEqual([
|
||||
{ key: "INPUT_2", label: "Input 2" },
|
||||
{ key: "INPUT_1", label: "Input 1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty array when there are no placeholders", () => {
|
||||
expect(extractSnippetInputs("uptime")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasSnippetInputs", () => {
|
||||
it("returns true when a snippet has an input placeholder", () => {
|
||||
expect(hasSnippetInputs("echo $INPUT_1")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when a snippet has no input placeholder", () => {
|
||||
expect(hasSnippetInputs("echo $HOST")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSnippetContent", () => {
|
||||
const host = { ip: "10.0.0.5", username: "root", port: 22, name: "web-01" };
|
||||
|
||||
it("substitutes host variables", () => {
|
||||
expect(
|
||||
resolveSnippetContent("ssh $USER@$HOST -p $PORT # $NAME", host),
|
||||
).toBe("ssh root@10.0.0.5 -p 22 # web-01");
|
||||
});
|
||||
|
||||
it("supports brace syntax for host variables", () => {
|
||||
expect(resolveSnippetContent("ping ${HOST}", host)).toBe("ping 10.0.0.5");
|
||||
});
|
||||
|
||||
it("substitutes input placeholders", () => {
|
||||
expect(
|
||||
resolveSnippetContent("nc -zv $HOST ${INPUT_1:Port}", host, {
|
||||
INPUT_1: "8080",
|
||||
}),
|
||||
).toBe("nc -zv 10.0.0.5 8080");
|
||||
});
|
||||
|
||||
it("leaves host variables literal when no host context is given", () => {
|
||||
expect(resolveSnippetContent("ping $HOST", null)).toBe("ping $HOST");
|
||||
});
|
||||
|
||||
it("leaves input placeholders literal when no value was supplied", () => {
|
||||
expect(resolveSnippetContent("echo $INPUT_1", null)).toBe("echo $INPUT_1");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user