diff --git a/src/components/AboutDialog.test.tsx b/src/components/AboutDialog.test.tsx index 95c32fb..4f7c06a 100644 --- a/src/components/AboutDialog.test.tsx +++ b/src/components/AboutDialog.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; +import * as checkUpdate from "../lib/check-update"; import * as tauri from "../lib/tauri"; import { AboutDialog } from "./AboutDialog"; @@ -7,6 +8,15 @@ afterEach(() => { vi.restoreAllMocks(); }); +const release = (version: string) => ({ + tagName: `v${version}`, + version, + htmlUrl: `https://github.com/oratis/Markup/releases/tag/v${version}`, + publishedAt: "2026-01-01T00:00:00Z", + name: null, + body: null, +}); + describe("AboutDialog", () => { it("shows the bundle id and renders the version once getVersion resolves", async () => { vi.spyOn(tauri, "getVersion").mockResolvedValue("0.9.9"); @@ -34,4 +44,38 @@ describe("AboutDialog", () => { fireEvent.click(screen.getByText(/^close$/i)); expect(onClose).toHaveBeenCalled(); }); + + it("Check for Updates reports when you're on the latest version", async () => { + vi.spyOn(tauri, "getVersion").mockResolvedValue("1.0.0"); + vi.spyOn(checkUpdate, "checkUpdateAgainstGithub").mockResolvedValue({ + hasUpdate: false, + current: "1.0.0", + latest: release("1.0.0"), + dismissed: false, + }); + render( {}} />); + fireEvent.click(screen.getByText("Check for Updates")); + await waitFor(() => + expect(screen.getByText("You're on the latest version")).toBeInTheDocument(), + ); + }); + + it("Check for Updates surfaces a newer release as a Get button", async () => { + vi.spyOn(tauri, "getVersion").mockResolvedValue("1.0.0"); + vi.spyOn(checkUpdate, "checkUpdateAgainstGithub").mockResolvedValue({ + hasUpdate: true, + current: "1.0.0", + latest: release("1.1.0"), + dismissed: false, + }); + render( {}} />); + fireEvent.click(screen.getByText("Check for Updates")); + await waitFor(() => expect(screen.getByText("Get v1.1.0")).toBeInTheDocument()); + }); + + it("always offers a Changelog link", () => { + vi.spyOn(tauri, "getVersion").mockResolvedValue("1.0.0"); + render( {}} />); + expect(screen.getByText("Changelog")).toBeInTheDocument(); + }); }); diff --git a/src/components/AboutDialog.tsx b/src/components/AboutDialog.tsx index 17aa293..453212d 100644 --- a/src/components/AboutDialog.tsx +++ b/src/components/AboutDialog.tsx @@ -1,4 +1,6 @@ +import { openUrl } from "@tauri-apps/plugin-opener"; import { useEffect, useState } from "react"; +import { type LatestRelease, checkUpdateAgainstGithub } from "../lib/check-update"; import { useT } from "../lib/i18n"; import { getVersion } from "../lib/tauri"; @@ -7,11 +9,20 @@ interface Props { } const REPO_URL = "https://github.com/oratis/Markup"; +const RELEASES_URL = `${REPO_URL}/releases`; const BUNDLE_ID = "com.appkon.markup"; +type UpdateState = + | { kind: "idle" } + | { kind: "checking" } + | { kind: "upToDate" } + | { kind: "available"; latest: LatestRelease } + | { kind: "error" }; + export function AboutDialog({ onClose }: Props) { const t = useT(); const [version, setVersion] = useState("…"); + const [update, setUpdate] = useState({ kind: "idle" }); useEffect(() => { getVersion() @@ -19,6 +30,18 @@ export function AboutDialog({ onClose }: Props) { .catch(() => setVersion("dev")); }, []); + const openExternal = (url: string) => { + openUrl(url).catch((e) => console.warn("about: failed to open url", e)); + }; + + const checkUpdates = async () => { + setUpdate({ kind: "checking" }); + const r = await checkUpdateAgainstGithub(); + if (!r.latest) setUpdate({ kind: "error" }); + else if (r.hasUpdate) setUpdate({ kind: "available", latest: r.latest }); + else setUpdate({ kind: "upToDate" }); + }; + return (
{BUNDLE_ID}
+ + {/* Update check + changelog */} +
+ {update.kind === "available" ? ( + + ) : ( + + )} + {update.kind === "upToDate" && ( +
{t("about.upToDate")}
+ )} + {update.kind === "error" && ( +
{t("about.checkFailed")}
+ )} + +
+