mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-11-12 15:51:40 +00:00
205 lines
6.4 KiB
TypeScript
205 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|||
|
|
import {
|
||
|
|
render,
|
||
|
|
screen,
|
||
|
|
waitFor,
|
||
|
|
cleanup,
|
||
|
|
fireEvent,
|
||
|
|
} from "@testing-library/react";
|
||
|
|
|
||
|
|
const mainAxios = vi.hoisted(() => ({
|
||
|
|
checkElectronUpdate: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("@/main-axios.ts", () => mainAxios);
|
||
|
|
|
||
|
|
vi.mock("react-i18next", () => ({
|
||
|
|
useTranslation: () => ({
|
||
|
|
t: (key: string, opts?: Record<string, unknown>) =>
|
||
|
|
opts ? `${key}:${JSON.stringify(opts)}` : key,
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// The whole component is a no-op outside Electron, so every test here needs the
|
||
|
|
// desktop build.
|
||
|
|
vi.mock("@/lib/electron", () => ({ isElectron: () => true }));
|
||
|
|
|
||
|
|
import { ElectronVersionCheck } from "../../user/ElectronVersionCheck";
|
||
|
|
|
||
|
|
const DISMISS_KEY = "electron-version-check-dismissed";
|
||
|
|
|
||
|
|
type ElectronTestWindow = Window &
|
||
|
|
typeof globalThis & {
|
||
|
|
electronAPI?: { getAppVersion?: () => Promise<string | undefined> };
|
||
|
|
};
|
||
|
|
|
||
|
|
function updateAvailable(localVersion: string, remoteVersion: string) {
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
status: "requires_update" as const,
|
||
|
|
localVersion,
|
||
|
|
remoteVersion,
|
||
|
|
latest_release: {
|
||
|
|
tag_name: `v${remoteVersion}`,
|
||
|
|
name: `Termix ${remoteVersion}`,
|
||
|
|
published_at: "2026-07-01T00:00:00Z",
|
||
|
|
html_url: `https://github.com/Termix-SSH/Termix/releases/tag/v${remoteVersion}`,
|
||
|
|
body: "",
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function upToDate(version: string) {
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
status: "up_to_date" as const,
|
||
|
|
localVersion: version,
|
||
|
|
remoteVersion: version,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Renders and waits until the check has resolved and the modal body is on
|
||
|
|
// screen, so that a later `not.toHaveBeenCalled()` is not vacuously true.
|
||
|
|
async function renderAndSettle(onContinue: () => void) {
|
||
|
|
render(<ElectronVersionCheck onContinue={onContinue} />);
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(mainAxios.checkElectronUpdate).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(screen.getByText("common.continue")).toBeTruthy();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
localStorage.clear();
|
||
|
|
mainAxios.checkElectronUpdate.mockReset();
|
||
|
|
(window as ElectronTestWindow).electronAPI = {
|
||
|
|
getAppVersion: vi.fn(async () => "2.6.0"),
|
||
|
|
} as unknown as ElectronTestWindow["electronAPI"];
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
cleanup();
|
||
|
|
localStorage.clear();
|
||
|
|
delete (window as ElectronTestWindow).electronAPI;
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("ElectronVersionCheck - dismissal is keyed on the offered version", () => {
|
||
|
|
it("still prompts when the stored dismissal is the local version and a newer release exists", async () => {
|
||
|
|
// Regression: the dismissal used to store the LOCAL version, and the
|
||
|
|
// up-to-date branch wrote it with no user action at all. A user who ran
|
||
|
|
// 2.6.0 while it was current had "2.6.0" stored, so once 2.6.1 shipped the
|
||
|
|
// modal was skipped on every launch - suppressed for exactly the users who
|
||
|
|
// had not updated yet.
|
||
|
|
localStorage.setItem(DISMISS_KEY, "2.6.0");
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue(
|
||
|
|
updateAvailable("2.6.0", "2.6.1"),
|
||
|
|
);
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
await renderAndSettle(onContinue);
|
||
|
|
|
||
|
|
expect(onContinue).not.toHaveBeenCalled();
|
||
|
|
expect(screen.getByText("versionCheck.updateRequired")).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("stores the remote version when the user dismisses the prompt", async () => {
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue(
|
||
|
|
updateAvailable("2.6.0", "2.6.1"),
|
||
|
|
);
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
await renderAndSettle(onContinue);
|
||
|
|
fireEvent.click(screen.getByText("common.continue"));
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(localStorage.getItem(DISMISS_KEY)).toBe("2.6.1");
|
||
|
|
});
|
||
|
|
expect(onContinue).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("skips the prompt on a later launch offering the same remote version", async () => {
|
||
|
|
localStorage.setItem(DISMISS_KEY, "2.6.1");
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue(
|
||
|
|
updateAvailable("2.6.0", "2.6.1"),
|
||
|
|
);
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
render(<ElectronVersionCheck onContinue={onContinue} />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(onContinue).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("prompts again once a newer release than the dismissed one appears", async () => {
|
||
|
|
localStorage.setItem(DISMISS_KEY, "2.6.1");
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue(
|
||
|
|
updateAvailable("2.6.0", "2.7.0"),
|
||
|
|
);
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
await renderAndSettle(onContinue);
|
||
|
|
|
||
|
|
expect(onContinue).not.toHaveBeenCalled();
|
||
|
|
expect(screen.getByText("versionCheck.updateRequired")).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keys a dismissed beta prompt on the remote version too", async () => {
|
||
|
|
// Local ahead of the published release: dismissing must still record what
|
||
|
|
// was offered, so the next release re-prompts.
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue({
|
||
|
|
success: true,
|
||
|
|
status: "beta" as const,
|
||
|
|
localVersion: "2.7.0-beta",
|
||
|
|
remoteVersion: "2.6.1",
|
||
|
|
});
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
await renderAndSettle(onContinue);
|
||
|
|
// Both the modal title and the alert heading use this key.
|
||
|
|
expect(screen.getAllByText("versionCheck.betaVersion").length).toBe(2);
|
||
|
|
fireEvent.click(screen.getByText("common.continue"));
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(localStorage.getItem(DISMISS_KEY)).toBe("2.6.1");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("continues without prompting when up to date, recording the offered version", async () => {
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue(upToDate("2.6.1"));
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
render(<ElectronVersionCheck onContinue={onContinue} />);
|
||
|
|
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(onContinue).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
expect(localStorage.getItem(DISMISS_KEY)).toBe("2.6.1");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not record a dismissal when the check itself fails", async () => {
|
||
|
|
mainAxios.checkElectronUpdate.mockResolvedValue({
|
||
|
|
success: false,
|
||
|
|
error: "Check failed",
|
||
|
|
});
|
||
|
|
const onContinue = vi.fn();
|
||
|
|
|
||
|
|
await renderAndSettle(onContinue);
|
||
|
|
|
||
|
|
// No offered version exists, so nothing may be written automatically and
|
||
|
|
// nothing may be suppressed.
|
||
|
|
expect(localStorage.getItem(DISMISS_KEY)).toBeNull();
|
||
|
|
expect(onContinue).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByText("common.continue"));
|
||
|
|
|
||
|
|
// Dismissing falls back to the local version, which cannot match a later
|
||
|
|
// release and so cannot suppress its prompt.
|
||
|
|
await waitFor(() => {
|
||
|
|
expect(localStorage.getItem(DISMISS_KEY)).toBe("2.6.0");
|
||
|
|
});
|
||
|
|
expect(onContinue).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
});
|