mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-11-12 15:26:36 +00:00
* 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>
214 lines
5.7 KiB
TypeScript
214 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
baselineSeen,
|
|
computeFlow,
|
|
needsFullFlow,
|
|
seenFor,
|
|
} from "@/onboarding/onboarding-flow";
|
|
import {
|
|
mergeStepDefs,
|
|
sectionPosition,
|
|
type OnboardingCtx,
|
|
type OnboardingStepDef,
|
|
} from "@/onboarding/onboarding-registry";
|
|
import { defaultOnboardingState } from "@/types/ui-preferences";
|
|
|
|
const Noop = () => null;
|
|
|
|
function step(
|
|
id: string,
|
|
position: number,
|
|
extra: Partial<OnboardingStepDef> = {},
|
|
): OnboardingStepDef {
|
|
return {
|
|
id,
|
|
version: 1,
|
|
titleKey: id,
|
|
Component: Noop,
|
|
audience: "all",
|
|
position,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function ctx(overrides: Partial<OnboardingCtx> = {}): OnboardingCtx {
|
|
return {
|
|
mode: "partial",
|
|
isAdmin: false,
|
|
isDesktop: false,
|
|
has: () => false,
|
|
pluginSetupPending: false,
|
|
pluginsManagedElsewhere: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const steps = [
|
|
step("welcome", 0),
|
|
step("appearance", 40, { version: 2 }),
|
|
step("rd:guacd", sectionPosition("setup"), {
|
|
pluginId: "rd",
|
|
audience: "admin",
|
|
}),
|
|
step("ai:assistant", sectionPosition("explore"), {
|
|
pluginId: "ai",
|
|
permission: "ai.use",
|
|
}),
|
|
step("done", 100),
|
|
];
|
|
|
|
const ids = (list: OnboardingStepDef[]) => list.map((s) => s.id);
|
|
|
|
describe("computeFlow", () => {
|
|
it("shows every relevant step on a full run", () => {
|
|
expect(ids(computeFlow(steps, {}, ctx({ mode: "full" })))).toEqual([
|
|
"welcome",
|
|
"appearance",
|
|
"done",
|
|
]);
|
|
});
|
|
|
|
it("filters admin and permission steps", () => {
|
|
const flow = computeFlow(
|
|
steps,
|
|
{},
|
|
ctx({ mode: "rerun", isAdmin: true, has: (p) => p === "ai.use" }),
|
|
);
|
|
expect(ids(flow)).toEqual([
|
|
"welcome",
|
|
"appearance",
|
|
"rd:guacd",
|
|
"ai:assistant",
|
|
"done",
|
|
]);
|
|
});
|
|
|
|
it("shows only unseen or bumped steps on a partial run", () => {
|
|
const flow = computeFlow(
|
|
steps,
|
|
{ welcome: 1, appearance: 1, "rd:guacd": 1, done: 1 },
|
|
ctx({ isAdmin: true, has: () => true }),
|
|
);
|
|
expect(ids(flow)).toEqual(["appearance", "ai:assistant"]);
|
|
});
|
|
|
|
it("is empty when everything is seen", () => {
|
|
expect(
|
|
computeFlow(
|
|
steps,
|
|
{ welcome: 1, appearance: 2, "rd:guacd": 1, done: 1 },
|
|
ctx({ isAdmin: true }),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("includes a seen step that forces itself pending", () => {
|
|
const picker = step("plugins", 10, {
|
|
forcePending: (c) => c.pluginSetupPending,
|
|
});
|
|
expect(
|
|
ids(
|
|
computeFlow(
|
|
[picker],
|
|
{ plugins: 1 },
|
|
ctx({ pluginSetupPending: true }),
|
|
),
|
|
),
|
|
).toEqual(["plugins"]);
|
|
expect(computeFlow([picker], { plugins: 1 }, ctx())).toEqual([]);
|
|
});
|
|
|
|
it("drops a step whose relevance check throws", () => {
|
|
const broken = step("x:y", 50, {
|
|
isRelevant: () => {
|
|
throw new Error("boom");
|
|
},
|
|
});
|
|
expect(computeFlow([broken], {}, ctx())).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("seen bookkeeping", () => {
|
|
it("records every step in a run at its version", () => {
|
|
expect(seenFor(steps.slice(0, 2))).toEqual({ welcome: 1, appearance: 2 });
|
|
});
|
|
|
|
it("baselines only plugin steps", () => {
|
|
expect(baselineSeen(steps)).toEqual({ "rd:guacd": 1, "ai:assistant": 1 });
|
|
});
|
|
|
|
it("needs the full run only for someone who never finished one", () => {
|
|
expect(needsFullFlow(defaultOnboardingState())).toBe(true);
|
|
expect(
|
|
needsFullFlow({ ...defaultOnboardingState(), seen: { welcome: 1 } }),
|
|
).toBe(false);
|
|
expect(
|
|
needsFullFlow({ ...defaultOnboardingState(), completedAt: "x" }),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("mergeStepDefs", () => {
|
|
it("orders by position and lets a registered step win over a legacy one", () => {
|
|
const legacy = step("ai:assistant", 80, { titleKey: "old" });
|
|
const registered = step("ai:assistant", 50, { titleKey: "new" });
|
|
const merged = mergeStepDefs(
|
|
[step("welcome", 0), step("done", 100)],
|
|
[registered],
|
|
[legacy, step("x:legacy", 80)],
|
|
);
|
|
expect(merged.map((s) => [s.id, s.titleKey])).toEqual([
|
|
["welcome", "welcome"],
|
|
["ai:assistant", "new"],
|
|
["x:legacy", "x:legacy"],
|
|
["done", "done"],
|
|
]);
|
|
});
|
|
|
|
it("places plugin sections between the core steps", () => {
|
|
expect(sectionPosition("setup")).toBeGreaterThan(40);
|
|
expect(sectionPosition("setup")).toBeLessThan(60);
|
|
expect(sectionPosition("explore", 1000)).toBeLessThan(90);
|
|
expect(sectionPosition("security")).toBeLessThan(100);
|
|
});
|
|
});
|
|
|
|
describe("start section and explicit position", () => {
|
|
it("puts the start section before the setup section", () => {
|
|
expect(sectionPosition("start")).toBe(25);
|
|
expect(sectionPosition("start")).toBeLessThan(sectionPosition("setup"));
|
|
});
|
|
|
|
it("uses an explicit position over the section and clamps it", () => {
|
|
expect(sectionPosition("explore", 0, 30)).toBe(30);
|
|
expect(sectionPosition("explore", 0, -5)).toBe(1);
|
|
expect(sectionPosition("explore", 0, 500)).toBe(99);
|
|
});
|
|
});
|
|
|
|
describe("replaced steps", () => {
|
|
const replacing = step("layout:preset", 30, {
|
|
pluginId: "layout",
|
|
replaces: ["preset"],
|
|
});
|
|
|
|
it("is not pending for someone who saw the step it replaces", () => {
|
|
const flow = computeFlow([replacing], { preset: 1 }, ctx());
|
|
expect(flow).toEqual([]);
|
|
});
|
|
|
|
it("is pending for someone who saw neither", () => {
|
|
expect(ids(computeFlow([replacing], {}, ctx()))).toEqual(["layout:preset"]);
|
|
});
|
|
|
|
it("is pending again when its own version goes up", () => {
|
|
const bumped = { ...replacing, version: 2 };
|
|
expect(ids(computeFlow([bumped], { preset: 1 }, ctx()))).toEqual([
|
|
"layout:preset",
|
|
]);
|
|
expect(
|
|
computeFlow([bumped], { preset: 1, "layout:preset": 2 }, ctx()),
|
|
).toEqual([]);
|
|
});
|
|
});
|