mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
The in-app updater only follows stable releases and its manifest notes just say "Release vX", so users jumping between stable versions never saw what the versions in between changed. The full release notes are too long for a popup. - Each version gets src/whats-new/<version>.json: up to 4 one-line items (new / fix / improve) in zh, zh-TW, en and ja, bundled into the app at build time. "items": [] marks a version that should not pop up. - On the first launch after an update, a dialog lists every non-empty version between the last one seen and the current one, newest first. Older versions beyond the first three are collapsed. Each version links to its page in the website changelog. - The last seen version is stored per device in settings.json (whatsNewSeenVersion) and only moves forward, so a downgrade and re-upgrade does not repeat anything. Without a record, only the current version is shown. When there is nothing to show, the version is recorded silently so the next update has a correct starting point. - The dialog waits for the welcome and new-layout dialogs. Confirming either of them also records the current version, so a 3.x user does not get two dialogs in a row. - Settings > About gets a "Recent updates" button to open the summaries again. The 4.0.0 summary is included so that list starts from 4.0; no update popup can reach it, since only 4.0.2 and later record a version. - The release workflow first requires the tagged version's summary file and validates every bundled summary with the unit test, since a malformed item would crash the dialog and put the whole app on the error screen.
217 lines
6.8 KiB
TypeScript
217 lines
6.8 KiB
TypeScript
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { WhatsNewEntry } from "@/lib/whatsNew";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
getAll: vi.fn(),
|
|
save: vi.fn(),
|
|
openExternal: vi.fn(),
|
|
version: "4.0.4",
|
|
settings: undefined as
|
|
| {
|
|
firstRunNoticeConfirmed?: boolean;
|
|
newLayoutNoticeConfirmed?: boolean;
|
|
whatsNewSeenVersion?: string;
|
|
}
|
|
| undefined,
|
|
entries: [] as WhatsNewEntry[],
|
|
}));
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
providersApi: { getAll: mocks.getAll },
|
|
settingsApi: { save: mocks.save, openExternal: mocks.openExternal },
|
|
}));
|
|
vi.mock("@/lib/query", () => ({
|
|
useSettingsQuery: () => ({ data: mocks.settings }),
|
|
}));
|
|
vi.mock("@tauri-apps/api/app", () => ({
|
|
getVersion: async () => mocks.version,
|
|
}));
|
|
vi.mock("@/lib/whatsNew", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("@/lib/whatsNew")>()),
|
|
get WHATS_NEW_ENTRIES() {
|
|
return mocks.entries;
|
|
},
|
|
}));
|
|
|
|
import { WhatsNewNotice } from "@/components/WhatsNewDialog";
|
|
|
|
const entry = (version: string, ...texts: string[]): WhatsNewEntry => ({
|
|
version,
|
|
items: texts.map((text) => ({
|
|
type: "fix",
|
|
zh: text,
|
|
"zh-TW": text,
|
|
en: `${text}-en`,
|
|
ja: `${text}-ja`,
|
|
})),
|
|
});
|
|
|
|
function renderNotice() {
|
|
const client = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
return render(
|
|
<QueryClientProvider client={client}>
|
|
<WhatsNewNotice />
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
const title = () => screen.queryByText("whatsNew.updatedTo");
|
|
|
|
/** 「不该弹」的用例:等异步查询都落定后再断言 */
|
|
const settle = () =>
|
|
act(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
});
|
|
|
|
describe("WhatsNewNotice", () => {
|
|
beforeEach(() => {
|
|
mocks.version = "4.0.4";
|
|
mocks.settings = {
|
|
firstRunNoticeConfirmed: true,
|
|
newLayoutNoticeConfirmed: true,
|
|
whatsNewSeenVersion: "4.0.1",
|
|
};
|
|
mocks.entries = [
|
|
entry("4.1.0", "未来版本"),
|
|
entry("4.0.4", "第四版甲", "第四版乙"),
|
|
entry("4.0.3"),
|
|
entry("4.0.2", "第二版"),
|
|
entry("4.0.1", "第一版"),
|
|
];
|
|
mocks.save.mockReset().mockResolvedValue(true);
|
|
mocks.openExternal.mockReset().mockResolvedValue(undefined);
|
|
mocks.getAll.mockReset().mockResolvedValue({ p1: { id: "p1" } });
|
|
});
|
|
|
|
it("lists every version since the last one seen and records the current version", async () => {
|
|
renderNotice();
|
|
expect(await screen.findByText("whatsNew.updatedTo")).toBeInTheDocument();
|
|
expect(screen.getByText("whatsNew.since")).toBeInTheDocument();
|
|
expect(screen.getByText("v4.0.4")).toBeInTheDocument();
|
|
expect(screen.getByText("第四版乙")).toBeInTheDocument();
|
|
expect(screen.getByText("v4.0.2")).toBeInTheDocument();
|
|
expect(screen.queryByText("v4.0.1")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("v4.0.3")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("v4.1.0")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "whatsNew.confirm" }));
|
|
await waitFor(() => expect(title()).not.toBeInTheDocument());
|
|
expect(mocks.save).toHaveBeenCalledWith(
|
|
expect.objectContaining({ whatsNewSeenVersion: "4.0.4" }),
|
|
);
|
|
});
|
|
|
|
it("shows only the current version when nothing was recorded before", async () => {
|
|
mocks.settings = {
|
|
firstRunNoticeConfirmed: true,
|
|
newLayoutNoticeConfirmed: true,
|
|
};
|
|
renderNotice();
|
|
expect(await screen.findByText("v4.0.4")).toBeInTheDocument();
|
|
expect(screen.queryByText("v4.0.2")).not.toBeInTheDocument();
|
|
expect(screen.queryByText("whatsNew.since")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("stays closed when the current version was already seen", async () => {
|
|
mocks.settings!.whatsNewSeenVersion = "4.0.4";
|
|
renderNotice();
|
|
await settle();
|
|
expect(title()).not.toBeInTheDocument();
|
|
expect(mocks.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stays closed after a downgrade and keeps the higher version", async () => {
|
|
mocks.version = "4.0.2";
|
|
mocks.settings!.whatsNewSeenVersion = "4.0.4";
|
|
renderNotice();
|
|
await settle();
|
|
expect(title()).not.toBeInTheDocument();
|
|
expect(mocks.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("silently records the version when there is nothing to show", async () => {
|
|
mocks.version = "4.0.3";
|
|
mocks.settings!.whatsNewSeenVersion = "4.0.2";
|
|
renderNotice();
|
|
await waitFor(() =>
|
|
expect(mocks.save).toHaveBeenCalledWith(
|
|
expect.objectContaining({ whatsNewSeenVersion: "4.0.3" }),
|
|
),
|
|
);
|
|
expect(title()).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("waits for the welcome dialog on a fresh install", async () => {
|
|
mocks.settings = { firstRunNoticeConfirmed: undefined };
|
|
renderNotice();
|
|
await settle();
|
|
expect(title()).not.toBeInTheDocument();
|
|
expect(mocks.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("waits while the new layout dialog is showing", async () => {
|
|
mocks.settings = {
|
|
firstRunNoticeConfirmed: true,
|
|
whatsNewSeenVersion: "4.0.1",
|
|
};
|
|
renderNotice();
|
|
await waitFor(() => expect(mocks.getAll).toHaveBeenCalled());
|
|
await settle();
|
|
expect(title()).not.toBeInTheDocument();
|
|
expect(mocks.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows when the new layout dialog does not apply", async () => {
|
|
mocks.settings = {
|
|
firstRunNoticeConfirmed: true,
|
|
whatsNewSeenVersion: "4.0.1",
|
|
};
|
|
mocks.getAll.mockResolvedValue({});
|
|
renderNotice();
|
|
expect(await screen.findByText("whatsNew.updatedTo")).toBeInTheDocument();
|
|
});
|
|
|
|
it("collapses older versions and opens the changelog list", async () => {
|
|
mocks.settings!.whatsNewSeenVersion = "4.0.0";
|
|
mocks.entries = [
|
|
entry("4.0.4", "第四版"),
|
|
entry("4.0.3", "第三版"),
|
|
entry("4.0.2", "第二版"),
|
|
entry("4.0.1", "第一版"),
|
|
];
|
|
renderNotice();
|
|
await screen.findByText("v4.0.4");
|
|
expect(screen.queryByText("v4.0.1")).not.toBeInTheDocument();
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "whatsNew.showEarlier" }),
|
|
);
|
|
expect(screen.getByText("v4.0.1")).toBeInTheDocument();
|
|
|
|
fireEvent.click(
|
|
screen.getAllByRole("button", { name: "whatsNew.versionDetails" })[0],
|
|
);
|
|
expect(mocks.openExternal).toHaveBeenCalledWith(
|
|
"https://ccswitch.io/zh/changelog/4.0.4",
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "whatsNew.viewFull" }));
|
|
expect(mocks.openExternal).toHaveBeenLastCalledWith(
|
|
"https://ccswitch.io/zh/changelog",
|
|
);
|
|
await waitFor(() => expect(title()).not.toBeInTheDocument());
|
|
expect(mocks.save).toHaveBeenCalledWith(
|
|
expect.objectContaining({ whatsNewSeenVersion: "4.0.4" }),
|
|
);
|
|
});
|
|
});
|