mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: desktop app auto updates with automatic, ask first or never modes
This commit is contained in:
1 parent
8403449669
commit
1aa1bde5f7
33 files changed
+1728
-177
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { updateToastFor, type DesktopUpdateState } from "@/lib/desktop-updates";
|
||||
|
||||
function state(patch: Partial<DesktopUpdateState>): DesktopUpdateState {
|
||||
return {
|
||||
kind: "auto",
|
||||
mode: "prompt",
|
||||
currentVersion: "26.10.1",
|
||||
status: "available",
|
||||
version: "26.10.2",
|
||||
releaseUrl: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
describe("updateToastFor", () => {
|
||||
it("asks before downloading in ask first mode", () => {
|
||||
expect(updateToastFor(state({}))).toBe("prompt");
|
||||
});
|
||||
|
||||
it("stays quiet while automatic mode downloads", () => {
|
||||
expect(updateToastFor(state({ mode: "automatic" }))).toBeNull();
|
||||
expect(
|
||||
updateToastFor(state({ mode: "automatic", status: "downloading" })),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("offers a restart once downloaded", () => {
|
||||
expect(updateToastFor(state({ status: "downloaded" }))).toBe("ready");
|
||||
expect(
|
||||
updateToastFor(state({ mode: "automatic", status: "downloaded" })),
|
||||
).toBe("ready");
|
||||
});
|
||||
|
||||
it("links the release for installs that can not update", () => {
|
||||
expect(updateToastFor(state({ kind: "manual" }))).toBe("manual");
|
||||
});
|
||||
|
||||
it("never toasts when updates are off", () => {
|
||||
expect(updateToastFor(state({ mode: "never" }))).toBeNull();
|
||||
expect(
|
||||
updateToastFor(state({ mode: "never", status: "downloaded" })),
|
||||
).toBeNull();
|
||||
expect(updateToastFor(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -93,6 +93,22 @@ describe("CORE_STEPS", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("asks how the desktop app updates when it has an updater", () => {
|
||||
const win = window as unknown as { electronAPI?: unknown };
|
||||
win.electronAPI = { isElectron: true, updates: {} };
|
||||
try {
|
||||
expect(ids(ctx({ isDesktop: true }))).toEqual([
|
||||
"desktop-sync",
|
||||
"desktop-updates",
|
||||
"preset",
|
||||
"appearance",
|
||||
]);
|
||||
expect(ids(ctx())).not.toContain("desktop-updates");
|
||||
} finally {
|
||||
delete win.electronAPI;
|
||||
}
|
||||
});
|
||||
|
||||
it("offers account security only when there is something to set up", () => {
|
||||
expect(ids(ctx({ canEnrollSecondFactor: true }))).toEqual([
|
||||
"preset",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from "@testing-library/react";
|
||||
import { DesktopUpdatesCard } from "@/settings/DesktopUpdatesCard";
|
||||
import type { DesktopUpdateState } from "@/lib/desktop-updates";
|
||||
|
||||
type TestWindow = { electronAPI?: unknown };
|
||||
|
||||
function state(patch: Partial<DesktopUpdateState> = {}): DesktopUpdateState {
|
||||
return {
|
||||
kind: "auto",
|
||||
mode: "prompt",
|
||||
currentVersion: "26.10.1",
|
||||
status: "idle",
|
||||
version: null,
|
||||
releaseUrl: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function bridge(initial: DesktopUpdateState) {
|
||||
return {
|
||||
getState: vi.fn(async () => initial),
|
||||
check: vi.fn(async () => initial),
|
||||
download: vi.fn(async () => initial),
|
||||
install: vi.fn(async () => true),
|
||||
setMode: vi.fn(async () => initial),
|
||||
onState: vi.fn(() => () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function renderWith(initial: DesktopUpdateState) {
|
||||
const api = bridge(initial);
|
||||
(window as TestWindow).electronAPI = { isElectron: true, updates: api };
|
||||
await act(async () => {
|
||||
render(<DesktopUpdatesCard />);
|
||||
});
|
||||
return api;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
delete (window as TestWindow).electronAPI;
|
||||
});
|
||||
|
||||
describe("DesktopUpdatesCard", () => {
|
||||
it("renders nothing in the browser", () => {
|
||||
const { container } = render(<DesktopUpdatesCard />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("checks on demand", async () => {
|
||||
const api = await renderWith(state());
|
||||
fireEvent.click(screen.getByText("desktopUpdates.checkNow"));
|
||||
expect(api.check).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("offers the download in ask first mode", async () => {
|
||||
const api = await renderWith(
|
||||
state({ status: "available", version: "26.10.2" }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("desktopUpdates.download"));
|
||||
expect(api.download).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restarts into a downloaded update", async () => {
|
||||
const api = await renderWith(
|
||||
state({ status: "downloaded", version: "26.10.2" }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("desktopUpdates.restart"));
|
||||
expect(api.install).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("links the release for installs that can not update", async () => {
|
||||
await renderWith(
|
||||
state({
|
||||
kind: "manual",
|
||||
status: "available",
|
||||
version: "26.10.2",
|
||||
releaseUrl: "https://example.com/r",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByText("desktopUpdates.download").closest("a"),
|
||||
).toHaveProperty("href", "https://example.com/r");
|
||||
});
|
||||
|
||||
it("hides on dev builds", async () => {
|
||||
await renderWith(state({ kind: "disabled" }));
|
||||
expect(screen.queryByText("desktopUpdates.title")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,16 @@ describe("settings pages", () => {
|
||||
expect(admin).toContain("security");
|
||||
});
|
||||
|
||||
it("shows Updates to everyone on desktop but only admins in the browser", () => {
|
||||
const ids = (isAdmin: boolean, isElectron: boolean) =>
|
||||
visibleSettingsPages(CORE_SETTINGS_PAGES, { isAdmin, isElectron }).map(
|
||||
(page) => page.id,
|
||||
);
|
||||
expect(ids(false, true)).toContain("updates");
|
||||
expect(ids(false, false)).not.toContain("updates");
|
||||
expect(ids(true, false)).toContain("updates");
|
||||
});
|
||||
|
||||
it("finds a page by a label shown on it, not only its name", () => {
|
||||
const hits = searchSettingsPages(CORE_SETTINGS_PAGES, "theme", translate);
|
||||
expect(hits.some((hit) => hit.page.id === "appearance")).toBe(true);
|
||||
|
||||
@@ -38,8 +38,12 @@ describe("onboarding seen map", () => {
|
||||
});
|
||||
|
||||
it("legacy seen covers every core step at launch", () => {
|
||||
// Steps added later stay out so upgraded users still see them.
|
||||
const addedLater = ["desktop-updates"];
|
||||
expect(Object.keys(LEGACY_SEEN).sort()).toEqual(
|
||||
Object.keys(CORE_ONBOARDING_STEPS).sort(),
|
||||
Object.keys(CORE_ONBOARDING_STEPS)
|
||||
.filter((key) => !addedLater.includes(key))
|
||||
.sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user