mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
mergeLocalMacros,
|
|
parseTerminalMacros,
|
|
sanitizeTerminalMacros,
|
|
runTerminalMacro,
|
|
type MacroStep,
|
|
type TerminalMacro,
|
|
} from "../../../src/frontend/macros/terminal-macros";
|
|
|
|
function macro(steps: MacroStep[]): TerminalMacro {
|
|
return {
|
|
id: "macro-1",
|
|
name: "Test",
|
|
steps,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
describe("terminal macro runner", () => {
|
|
it("runs send, delay, regex wait, condition, and repeat steps", async () => {
|
|
vi.useFakeTimers();
|
|
const sent: string[] = [];
|
|
let listener = (_data: string) => {};
|
|
const run = runTerminalMacro(
|
|
macro([
|
|
{ id: "send", type: "send", text: "status", pressEnter: true },
|
|
{
|
|
id: "wait",
|
|
type: "wait",
|
|
pattern: "READY\\s+42",
|
|
timeoutMs: 1000,
|
|
onTimeout: "stop",
|
|
},
|
|
{
|
|
id: "if",
|
|
type: "if",
|
|
pattern: "READY",
|
|
then: [
|
|
{
|
|
id: "repeat",
|
|
type: "repeat",
|
|
count: 2,
|
|
steps: [
|
|
{ id: "ok", type: "send", text: "ok", pressEnter: false },
|
|
],
|
|
},
|
|
],
|
|
else: [],
|
|
},
|
|
]),
|
|
{
|
|
send: (data) => sent.push(data),
|
|
subscribe: (next) => {
|
|
listener = next;
|
|
return () => {};
|
|
},
|
|
},
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
listener("READY 42");
|
|
await run;
|
|
expect(sent).toEqual(["status\r", "ok", "ok"]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("stops when a required regex times out", async () => {
|
|
vi.useFakeTimers();
|
|
const run = runTerminalMacro(
|
|
macro([
|
|
{
|
|
id: "wait",
|
|
type: "wait",
|
|
pattern: "never",
|
|
timeoutMs: 100,
|
|
onTimeout: "stop",
|
|
},
|
|
]),
|
|
{ send: () => {}, subscribe: () => () => {} },
|
|
);
|
|
const rejection = expect(run).rejects.toThrow("Timed out");
|
|
await vi.advanceTimersByTimeAsync(101);
|
|
await rejection;
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("sanitizes persisted macro bounds and drops unknown steps", () => {
|
|
const parsed = parseTerminalMacros(
|
|
JSON.stringify([
|
|
{
|
|
id: "m",
|
|
name: "Macro",
|
|
steps: [
|
|
{ id: "d", type: "delay", milliseconds: 999_999 },
|
|
{ id: "r", type: "repeat", count: 1000, steps: [] },
|
|
{ id: "bad", type: "execute-javascript", code: "nope" },
|
|
],
|
|
},
|
|
]),
|
|
);
|
|
expect(parsed[0].steps).toEqual([
|
|
{ id: "d", type: "delay", milliseconds: 300_000 },
|
|
{ id: "r", type: "repeat", count: 100, steps: [] },
|
|
]);
|
|
});
|
|
it("matches literal text without regex escaping when isRegex is false", async () => {
|
|
vi.useFakeTimers();
|
|
const sent: string[] = [];
|
|
let listener = (_data: string) => {};
|
|
const run = runTerminalMacro(
|
|
macro([
|
|
{
|
|
id: "wait",
|
|
type: "wait",
|
|
pattern: "progress (1/2)",
|
|
isRegex: false,
|
|
timeoutMs: 1000,
|
|
onTimeout: "stop",
|
|
},
|
|
{ id: "send", type: "send", text: "done", pressEnter: false },
|
|
]),
|
|
{
|
|
send: (data) => sent.push(data),
|
|
subscribe: (next) => {
|
|
listener = next;
|
|
return () => {};
|
|
},
|
|
},
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
listener("progress (1/2)");
|
|
await run;
|
|
expect(sent).toEqual(["done"]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("falls back to literal matching when a regex is invalid", async () => {
|
|
vi.useFakeTimers();
|
|
const sent: string[] = [];
|
|
let listener = (_data: string) => {};
|
|
const run = runTerminalMacro(
|
|
macro([
|
|
{
|
|
id: "wait",
|
|
type: "wait",
|
|
pattern: "oops(",
|
|
isRegex: true,
|
|
timeoutMs: 1000,
|
|
onTimeout: "stop",
|
|
},
|
|
{ id: "send", type: "send", text: "ok", pressEnter: false },
|
|
]),
|
|
{
|
|
send: (data) => sent.push(data),
|
|
subscribe: (next) => {
|
|
listener = next;
|
|
return () => {};
|
|
},
|
|
},
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
listener("oops(");
|
|
await run;
|
|
expect(sent).toEqual(["ok"]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("branches on literal if-step text", async () => {
|
|
const sent: string[] = [];
|
|
await runTerminalMacro(
|
|
macro([
|
|
{
|
|
id: "if",
|
|
type: "if",
|
|
pattern: "a.b",
|
|
isRegex: false,
|
|
then: [{ id: "y", type: "send", text: "yes", pressEnter: false }],
|
|
else: [{ id: "n", type: "send", text: "no", pressEnter: false }],
|
|
},
|
|
]),
|
|
{ send: (data) => sent.push(data), subscribe: () => () => {} },
|
|
);
|
|
// "a.b" as a literal must not match "axb"
|
|
expect(sent).toEqual(["no"]);
|
|
});
|
|
});
|
|
|
|
describe("mergeLocalMacros", () => {
|
|
function storage(value: string | null) {
|
|
const store = new Map<string, string>();
|
|
if (value !== null) store.set("terminalMacros", value);
|
|
return {
|
|
store,
|
|
getItem: (key: string) => store.get(key) ?? null,
|
|
removeItem: (key: string) => void store.delete(key),
|
|
};
|
|
}
|
|
const saved = (id: string): TerminalMacro => ({ ...macro([]), id });
|
|
|
|
it("returns null when the browser kept nothing", () => {
|
|
expect(mergeLocalMacros([saved("a")], storage(null))).toBeNull();
|
|
});
|
|
|
|
it("adds the browser's macros the saved list does not have", () => {
|
|
const local = storage(
|
|
JSON.stringify([
|
|
{ id: "a", name: "Dupe", steps: [] },
|
|
{ id: "b", name: "Local", steps: [] },
|
|
]),
|
|
);
|
|
const merged = mergeLocalMacros([saved("a")], local);
|
|
expect(merged?.macros.map((m) => m.id)).toEqual(["a", "b"]);
|
|
expect(local.store.has("terminalMacros")).toBe(true);
|
|
merged?.clear();
|
|
expect(local.store.has("terminalMacros")).toBe(false);
|
|
});
|
|
|
|
it("clears the key when everything is already saved", () => {
|
|
const local = storage(JSON.stringify([{ id: "a", name: "A", steps: [] }]));
|
|
expect(mergeLocalMacros([saved("a")], local)).toBeNull();
|
|
expect(local.store.has("terminalMacros")).toBe(false);
|
|
});
|
|
|
|
it("reads a stored array as well as its JSON", () => {
|
|
expect(
|
|
sanitizeTerminalMacros([{ id: "x", name: "X", steps: [] }]).map(
|
|
(m) => m.id,
|
|
),
|
|
).toEqual(["x"]);
|
|
});
|
|
});
|