mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-04 17:45:24 +00:00
226 lines
6.7 KiB
TypeScript
226 lines
6.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
import i18next from "i18next";
|
|
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
import type { OnboardingPluginList } from "@/api/plugins-api";
|
|
|
|
const api = vi.hoisted(() => ({
|
|
getOnboardingPlugins: vi.fn(),
|
|
applyOnboardingPlugins: vi.fn(),
|
|
}));
|
|
vi.mock("@/api/plugins-api", () => api);
|
|
|
|
const loader = vi.hoisted(() => ({ syncPlugins: vi.fn(async () => {}) }));
|
|
vi.mock("@/plugin-host/loader", () => loader);
|
|
|
|
import { PluginsStep } from "@/onboarding/steps/PluginsStep";
|
|
import { defaultChoices } from "@/onboarding/plugin-choices";
|
|
|
|
function plugin(
|
|
id: string,
|
|
extra: Partial<OnboardingPluginList["plugins"][number]> = {},
|
|
) {
|
|
return {
|
|
id,
|
|
name: id.toUpperCase(),
|
|
description: `${id} plugin`,
|
|
icon: null,
|
|
category: "Terminal",
|
|
version: "1.0.0",
|
|
source: "bundled" as const,
|
|
state: "enabled",
|
|
dependencies: [],
|
|
recommended: false,
|
|
consent: false,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
const LIST: OnboardingPluginList = {
|
|
pending: true,
|
|
reason: "fresh",
|
|
managedByLinkedServer: false,
|
|
plugins: [
|
|
plugin("term", { recommended: true }),
|
|
plugin("base", { category: "Infrastructure" }),
|
|
plugin("addon", { category: "Infrastructure", dependencies: ["base"] }),
|
|
plugin("stats", { recommended: true, consent: true, state: "disabled" }),
|
|
],
|
|
};
|
|
|
|
describe("defaultChoices", () => {
|
|
it("keeps recommended plugins on and the rest off on a fresh install", () => {
|
|
expect(defaultChoices(LIST)).toEqual({
|
|
term: "enabled",
|
|
base: "disabled",
|
|
addon: "disabled",
|
|
stats: "enabled",
|
|
});
|
|
});
|
|
|
|
it("starts from how things are now on an upgraded install", () => {
|
|
expect(defaultChoices({ ...LIST, reason: "upgrade" })).toEqual({
|
|
term: "enabled",
|
|
base: "enabled",
|
|
addon: "enabled",
|
|
stats: "disabled",
|
|
});
|
|
});
|
|
|
|
it("starts from how things are now on a later run", () => {
|
|
expect(defaultChoices({ ...LIST, pending: false })).toEqual({
|
|
term: "enabled",
|
|
base: "enabled",
|
|
addon: "enabled",
|
|
stats: "disabled",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("PluginsStep", () => {
|
|
let beforeNext: (() => boolean | Promise<boolean>) | null = null;
|
|
|
|
beforeEach(() => {
|
|
beforeNext = null;
|
|
api.getOnboardingPlugins.mockResolvedValue(LIST);
|
|
api.applyOnboardingPlugins.mockReset();
|
|
loader.syncPlugins.mockClear();
|
|
});
|
|
|
|
async function renderStep() {
|
|
render(
|
|
<PluginsStep
|
|
mode="full"
|
|
isAdmin
|
|
isDesktop={false}
|
|
shellReady={false}
|
|
setCanContinue={() => {}}
|
|
setBeforeNext={(fn) => {
|
|
beforeNext = fn;
|
|
}}
|
|
/>,
|
|
);
|
|
await screen.findByText("TERM");
|
|
// findByText resolves on the DOM change, before the effect that hands
|
|
// over the loaded apply has run. Flush it so beforeNext is never stale.
|
|
await act(async () => {});
|
|
}
|
|
|
|
it("groups plugins by category and puts consent plugins in their own section", async () => {
|
|
await renderStep();
|
|
expect(screen.getByText("Terminal")).toBeTruthy();
|
|
expect(screen.getByText("Infrastructure")).toBeTruthy();
|
|
expect(screen.getByRole("checkbox")).toHaveProperty("checked", true);
|
|
});
|
|
|
|
it("tells an upgrading admin that nothing changes unless they say so", async () => {
|
|
api.getOnboardingPlugins.mockResolvedValue({ ...LIST, reason: "upgrade" });
|
|
await renderStep();
|
|
expect(screen.getByText(/onboarding.pluginsUpgradeNote/)).toBeTruthy();
|
|
});
|
|
|
|
it("keeps a dependency the admin kept something for, and says why", async () => {
|
|
await renderStep();
|
|
const addon = screen.getByRole("radiogroup", { name: "ADDON" });
|
|
fireEvent.click(within(addon).getByRole("radio", { name: /enabled|On/ }));
|
|
expect(
|
|
await screen.findByText("onboarding.pluginsKeptBecause"),
|
|
).toBeTruthy();
|
|
const base = screen.getByRole("radiogroup", { name: "BASE" });
|
|
expect(
|
|
within(base)
|
|
.getByRole("radio", { name: /enabled|On/ })
|
|
.getAttribute("aria-checked"),
|
|
).toBe("true");
|
|
});
|
|
|
|
it("applies the choices on Next and resyncs the plugin runtime", async () => {
|
|
api.applyOnboardingPlugins.mockResolvedValue({
|
|
resolved: {},
|
|
adjustments: [],
|
|
enabled: [],
|
|
disabled: ["base", "addon"],
|
|
removed: [],
|
|
failed: [],
|
|
});
|
|
api.getOnboardingPlugins.mockResolvedValueOnce(LIST).mockResolvedValue({
|
|
...LIST,
|
|
pending: false,
|
|
});
|
|
await renderStep();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await beforeNext!();
|
|
});
|
|
expect(ok).toBe(true);
|
|
expect(api.applyOnboardingPlugins).toHaveBeenCalledWith({
|
|
term: "enabled",
|
|
base: "disabled",
|
|
addon: "disabled",
|
|
stats: "enabled",
|
|
});
|
|
expect(loader.syncPlugins).toHaveBeenCalled();
|
|
});
|
|
|
|
it("stops once to show failures, then lets the admin move on", async () => {
|
|
api.applyOnboardingPlugins.mockResolvedValue({
|
|
resolved: {},
|
|
adjustments: [],
|
|
enabled: [],
|
|
disabled: [],
|
|
removed: [],
|
|
failed: [{ id: "base", error: "busy" }],
|
|
});
|
|
api.getOnboardingPlugins.mockResolvedValueOnce(LIST).mockResolvedValue({
|
|
...LIST,
|
|
pending: false,
|
|
});
|
|
await renderStep();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await beforeNext!();
|
|
});
|
|
expect(ok).toBe(false);
|
|
expect(screen.getByText("BASE: busy")).toBeTruthy();
|
|
await act(async () => {
|
|
ok = await beforeNext!();
|
|
});
|
|
expect(ok).toBe(true);
|
|
expect(api.applyOnboardingPlugins).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("shows plugin names and descriptions in the UI language", async () => {
|
|
const i18n = i18next.createInstance();
|
|
await i18n.use(initReactI18next).init({
|
|
lng: "zh-CN",
|
|
fallbackNS: "translation",
|
|
resources: {
|
|
"zh-CN": {
|
|
translation: {},
|
|
term: { plugin: { name: "终端", description: "终端插件" } },
|
|
stats: { plugin: { name: "统计" } },
|
|
},
|
|
},
|
|
});
|
|
render(
|
|
<I18nextProvider i18n={i18n}>
|
|
<PluginsStep
|
|
mode="full"
|
|
isAdmin
|
|
isDesktop={false}
|
|
shellReady={false}
|
|
setCanContinue={() => {}}
|
|
setBeforeNext={() => {}}
|
|
/>
|
|
</I18nextProvider>,
|
|
);
|
|
expect(await screen.findByText("终端")).toBeTruthy();
|
|
expect(screen.getByText("终端插件")).toBeTruthy();
|
|
expect(screen.getByRole("radiogroup", { name: "终端" })).toBeTruthy();
|
|
expect(screen.getByText("统计")).toBeTruthy();
|
|
expect(screen.getByText("stats plugin")).toBeTruthy();
|
|
expect(screen.getByText("BASE")).toBeTruthy();
|
|
expect(screen.getByText("base plugin")).toBeTruthy();
|
|
});
|
|
});
|