Files
spark-store/src/__tests__/unit/App.account-placeholders.test.ts
T

1236 lines
40 KiB
TypeScript

import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue";
import {
addFavoriteItem,
deleteFavoriteItem,
fetchRatingSummary,
fetchReviews,
fetchSyncedAppList,
createFavoriteFolder,
listDownloadedApps,
listFavoriteFolders,
listFavoriteItems,
uploadSyncedAppList,
} from "@/modules/backendApi";
import { setAuthSession } from "@/global/authState";
import type {
DownloadedAppList,
FavoriteFolder,
FavoriteItem,
SyncedAppList,
} from "@/global/typedefinition";
const invoke = vi.fn();
const favoriteFolders: FavoriteFolder[] = [
{
id: 7,
name: "默认收藏夹",
itemCount: 1,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
];
const favoriteItems: FavoriteItem[] = [
{
id: 11,
appKey: "app:office:wps",
pkgname: "wps",
name: "WPS",
category: "office",
iconUrl: "",
createdAt: "2026-05-18T00:00:00Z",
},
];
const createDeferred = <T>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});
return { promise, resolve };
};
const downloadedList = (
items: DownloadedAppList["items"],
): DownloadedAppList => ({
items,
total: items.length,
page: 1,
pageSize: 50,
});
const syncedList = (items: SyncedAppList["items"]): SyncedAppList => ({
snapshotName: "默认列表",
clientArch: "amd64",
distro: "deepin 25",
updatedAt: "2026-05-18T00:00:00Z",
items,
});
const setSecondUserSession = () => {
setAuthSession({
accessToken: "backend-token-b",
tokenType: "bearer",
user: {
id: 2,
flarumUserId: "84",
username: "second",
displayName: "Second User",
avatarUrl: "https://bbs.spark-app.store/avatar-b.png",
forumLevel: "用户",
forumGroups: ["用户"],
},
});
};
vi.mock("axios", () => {
const get = vi.fn(async (url: string) => {
if (url.includes("categories.json")) {
return { data: { office: { zh: "办公" } } };
}
if (url.includes("/office/applist.json")) {
return {
data: [
{
Name: "WPS",
Pkgname: "wps",
Version: "1.0.0",
Filename: "wps_1.0.0_amd64.deb",
Torrent_address: "",
Author: "",
Contributor: "",
Website: "",
Update: "",
Size: "",
More: "",
Tags: "",
img_urls: [],
icons: "",
},
],
};
}
return { data: [] };
});
const post = vi.fn(async () => ({ data: { ok: true } }));
return {
default: {
create: () => ({ get, post }),
},
};
});
vi.mock("@/modules/updateCenter", () => ({
createUpdateCenterStore: () => ({
isOpen: { value: false },
showCloseConfirm: { value: false },
showMigrationConfirm: { value: false },
searchQuery: { value: "" },
selectedTaskKeys: { value: new Set<string>() },
snapshot: {
value: { items: [], tasks: [], warnings: [], hasRunningTasks: false },
},
filteredItems: { value: [] },
allSelected: { value: false },
someSelected: { value: false },
bind: vi.fn(),
unbind: vi.fn(),
open: vi.fn(),
refresh: vi.fn(),
ignoreItem: vi.fn(),
unignoreItem: vi.fn(),
toggleSelection: vi.fn(),
toggleSelectAll: vi.fn(),
getSelectedItems: vi.fn(() => []),
closeNow: vi.fn(),
startSelected: vi.fn(),
requestClose: vi.fn(),
}),
}));
vi.mock("@/modules/backendApi", () => ({
addFavoriteItem: vi.fn(),
bulkDeleteFavoriteItems: vi.fn(),
createFavoriteFolder: vi.fn(),
deleteFavoriteItem: vi.fn(),
exchangeFlarumToken: vi.fn(),
fetchRatingSummary: vi.fn(async () => ({
averageRating: 5,
reviewCount: 1,
starCounts: { 5: 1 },
})),
fetchReviews: vi.fn(async () => [
{
id: 31,
rating: 5,
content: "profile entry",
version: "1.0.0",
packageArch: "amd64",
clientArch: "amd64",
distro: "deepin 25",
origin: "apm",
category: "office",
createdAt: "2026-05-20T00:00:00Z",
updatedAt: "2026-05-20T00:00:00Z",
userDisplayName: "Detail User",
userAvatarUrl: "https://bbs.spark-app.store/avatar-detail.png",
canDelete: false,
replies: [],
},
]),
fetchSyncedAppList: vi.fn(async () => null),
submitReview: vi.fn(),
likeReview: vi.fn(),
createReviewReply: vi.fn(),
deleteReview: vi.fn(),
likeReviewReply: vi.fn(),
deleteReviewReply: vi.fn(),
listDownloadedApps: vi.fn(async () => downloadedList([])),
listFavoriteFolders: vi.fn(async () => favoriteFolders),
listFavoriteItems: vi.fn(async () => favoriteItems),
uploadSyncedAppList: vi.fn(),
setBackendToken: vi.fn(),
}));
describe("App account placeholders", () => {
beforeEach(() => {
vi.clearAllMocks();
invoke.mockReset();
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
return [];
});
Object.assign(window.ipcRenderer, {
invoke,
on: vi.fn(),
off: vi.fn(),
send: vi.fn(),
});
window.apm_store.arch = "amd64";
localStorage.clear();
setAuthSession({
accessToken: "backend-token",
tokenType: "bearer",
user: {
id: 1,
flarumUserId: "42",
username: "momen",
displayName: "Momen",
avatarUrl: "https://bbs.spark-app.store/avatar.png",
coverUrl:
"https://bbs.spark-app.store/assets/covers/JizZCVjiSFASrEfp.jpg",
forumLevel: "管理员",
forumGroups: ["管理员"],
},
});
vi.mocked(fetchRatingSummary).mockResolvedValue({
averageRating: 5,
reviewCount: 1,
starCounts: { 5: 1 },
});
vi.mocked(fetchReviews).mockResolvedValue([
{
id: 31,
rating: 5,
content: "profile entry",
version: "1.0.0",
packageArch: "amd64",
clientArch: "amd64",
distro: "deepin 25",
origin: "apm",
category: "office",
createdAt: "2026-05-20T00:00:00Z",
updatedAt: "2026-05-20T00:00:00Z",
userDisplayName: "Detail User",
userAvatarUrl: "https://bbs.spark-app.store/avatar-detail.png",
canDelete: false,
replies: [],
},
]);
vi.stubGlobal(
"matchMedia",
vi.fn(() => ({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
);
vi.stubGlobal("scrollTo", vi.fn());
class MockIntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
});
it("shows user management as a global modal from the logged-in quick menu", async () => {
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
expect(await screen.findByText("wps · 1.0.0")).toBeTruthy();
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(
await screen.findByRole("dialog", { name: "用户管理" }),
).toBeTruthy();
const frame = await screen.findByTitle("星火账号用户管理");
expect(frame).toBeTruthy();
expect((frame as HTMLIFrameElement).src).toContain(
"account.spark-app.store",
);
expect(screen.getByText("wps · 1.0.0")).toBeTruthy();
expect(screen.queryByText("请登录后查看和管理账号信息。")).toBeNull();
});
it("opens an in-app profile modal from review author clicks", async () => {
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("WPS"));
await fireEvent.click(
await screen.findByRole("button", { name: "Detail User" }),
);
const dialog = await screen.findByRole("dialog", { name: "用户资料" });
expect(dialog).toBeTruthy();
expect(within(dialog).getByText("Detail User")).toBeTruthy();
});
it("enriches the current user's own review profile without a review user id", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
vi.mocked(fetchReviews).mockResolvedValueOnce([
{
id: 32,
rating: 5,
content: "own profile entry",
version: "1.0.0",
packageArch: "amd64",
clientArch: "amd64",
distro: "deepin 25",
origin: "apm",
category: "office",
createdAt: "2026-05-20T00:00:00Z",
updatedAt: "2026-05-20T00:00:00Z",
userDisplayName: "Momen",
userAvatarUrl: "",
isAuthor: true,
canDelete: false,
replies: [],
},
]);
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("WPS"));
await fireEvent.click(
await screen.findByRole("button", { name: "查看Momen的资料" }),
);
const dialog = await screen.findByRole("dialog", { name: "用户资料" });
expect(within(dialog).getByText("@momen")).toBeTruthy();
expect(within(dialog).getByText("管理员")).toBeTruthy();
expect(
within(dialog).getByRole("button", { name: "查看论坛资料" }),
).toBeTruthy();
expect(openSpy).not.toHaveBeenCalled();
await fireEvent.click(
within(dialog).getByRole("button", { name: "查看论坛资料" }),
);
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining("/u/momen"),
"_blank",
"noopener,noreferrer",
);
});
it("shows the favorites placeholder from the logged-in quick menu", async () => {
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(screen.queryByText("请登录后查看我的收藏。")).toBeNull();
});
it("shows favorite management as a standalone page without category pills", async () => {
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(await screen.findByText("默认收藏夹 (1)")).toBeTruthy();
expect(screen.queryByRole("button", { name: /办公/ })).toBeNull();
});
it("loads favorite state when opening app detail directly", async () => {
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(
await screen.findByRole("button", { name: "已收藏 · 默认收藏夹" }),
).toBeTruthy();
expect(listFavoriteFolders).toHaveBeenCalled();
expect(listFavoriteItems).toHaveBeenCalledWith(7);
});
it("shows a newly created favorite folder immediately on the management page", async () => {
const createdFolder: FavoriteFolder = {
id: 8,
name: "办公收藏",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
};
vi.spyOn(window, "prompt").mockReturnValue("办公收藏");
vi.mocked(createFavoriteFolder).mockResolvedValueOnce(createdFolder);
vi.mocked(listFavoriteFolders).mockResolvedValue(favoriteFolders);
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "新建收藏夹" }));
expect(createFavoriteFolder).toHaveBeenCalledWith("办公收藏");
expect(await screen.findByText("办公收藏 (0)")).toBeTruthy();
expect(await screen.findByText("当前收藏夹暂无应用。")).toBeTruthy();
});
it("shows detail favorite state for an app added to a non-active folder", async () => {
const folders = [
favoriteFolders[0],
{
id: 8,
name: "办公收藏",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
];
vi.mocked(listFavoriteFolders)
.mockResolvedValueOnce(folders)
.mockResolvedValueOnce(folders);
vi.mocked(listFavoriteItems).mockResolvedValue([]);
vi.mocked(addFavoriteItem).mockResolvedValueOnce({
...favoriteItems[0],
id: 12,
});
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "收藏" }));
await fireEvent.click(await screen.findByLabelText("收藏到 办公收藏"));
await fireEvent.click(screen.getByRole("button", { name: "保存收藏夹" }));
expect(
await screen.findByRole("button", { name: "已收藏 · 办公收藏" }),
).toBeTruthy();
});
it("adds a favorite through the fallback default folder", async () => {
vi.mocked(listFavoriteFolders)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce(favoriteFolders);
vi.mocked(listFavoriteItems).mockResolvedValueOnce([]);
vi.mocked(addFavoriteItem).mockResolvedValueOnce(favoriteItems[0]);
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "收藏" }));
await fireEvent.click(await screen.findByLabelText("收藏到 默认收藏夹"));
await fireEvent.click(screen.getByRole("button", { name: "保存收藏夹" }));
expect(addFavoriteItem).toHaveBeenCalledWith(
"default",
expect.objectContaining({ pkgname: "wps" }),
);
});
it("ignores stale favorite refreshes while opening the detail selector", async () => {
const refreshFolders = createDeferred<FavoriteFolder[]>();
const selectorFolders = createDeferred<FavoriteFolder[]>();
const staleFolder = {
id: 9,
name: "旧收藏夹",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
};
const currentFolder = {
id: 8,
name: "办公收藏",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
};
vi.mocked(listFavoriteFolders)
.mockReturnValueOnce(refreshFolders.promise)
.mockReturnValueOnce(selectorFolders.promise);
vi.mocked(listFavoriteItems).mockResolvedValue([]);
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByText("我的收藏"));
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "收藏" }));
selectorFolders.resolve([currentFolder]);
expect(await screen.findByLabelText("收藏到 办公收藏")).toBeTruthy();
refreshFolders.resolve([staleFolder]);
await refreshFolders.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.getByLabelText("收藏到 办公收藏")).toBeTruthy();
expect(screen.queryByLabelText("收藏到 旧收藏夹")).toBeNull();
});
it("shows detail favorite state from a non-active backend folder", async () => {
const folders = [
favoriteFolders[0],
{
id: 8,
name: "办公收藏",
itemCount: 1,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
];
vi.mocked(listFavoriteFolders).mockResolvedValueOnce(folders);
vi.mocked(listFavoriteItems)
.mockResolvedValueOnce([])
.mockResolvedValueOnce(favoriteItems);
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(
await screen.findByRole("button", { name: "已收藏 · 办公收藏" }),
).toBeTruthy();
});
it("saves favorite folder checkbox changes from the detail selector", async () => {
const folders = [
favoriteFolders[0],
{
id: 8,
name: "办公收藏",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
];
vi.mocked(listFavoriteFolders).mockResolvedValue(folders);
vi.mocked(listFavoriteItems).mockImplementation(async (folderId: number) =>
folderId === 7 ? favoriteItems : [],
);
vi.mocked(addFavoriteItem).mockResolvedValueOnce({
...favoriteItems[0],
id: 12,
});
vi.mocked(deleteFavoriteItem).mockResolvedValueOnce(undefined);
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
expect(
await screen.findByRole("button", { name: "已收藏 · 默认收藏夹" }),
).toBeTruthy();
await fireEvent.click(
screen.getByRole("button", { name: "已收藏 · 默认收藏夹" }),
);
await fireEvent.click(await screen.findByLabelText("收藏到 默认收藏夹"));
await fireEvent.click(await screen.findByLabelText("收藏到 办公收藏"));
await fireEvent.click(screen.getByRole("button", { name: "保存收藏夹" }));
expect(deleteFavoriteItem).toHaveBeenCalledWith(7, 11);
expect(addFavoriteItem).toHaveBeenCalledWith(
8,
expect.objectContaining({
pkgname: "wps",
}),
);
});
it("creates a folder from the favorite selector without closing it", async () => {
const createdFolder = {
id: 8,
name: "办公收藏",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
};
vi.spyOn(window, "prompt").mockReturnValue("办公收藏");
vi.mocked(createFavoriteFolder).mockResolvedValueOnce(createdFolder);
vi.mocked(listFavoriteFolders)
.mockResolvedValueOnce(favoriteFolders)
.mockResolvedValueOnce([favoriteFolders[0], createdFolder]);
vi.mocked(listFavoriteItems)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([]);
vi.mocked(addFavoriteItem).mockResolvedValueOnce(favoriteItems[0]);
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "收藏" }));
await fireEvent.click(
await screen.findByRole("button", { name: "新建收藏夹" }),
);
expect(createFavoriteFolder).toHaveBeenCalledWith("办公收藏");
expect(
await screen.findByRole("dialog", { name: "选择收藏夹" }),
).toBeTruthy();
expect(await screen.findByLabelText("收藏到 办公收藏")).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "保存收藏夹" }));
expect(addFavoriteItem).toHaveBeenCalledWith(
8,
expect.objectContaining({ pkgname: "wps" }),
);
});
it("refreshes installed apps before resolving favorite management state", async () => {
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "list-installed") {
return {
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "apm",
},
],
};
}
return [];
});
render(App);
await waitFor(() => {
expect(screen.getByText("2")).toBeTruthy();
});
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(await screen.findByText("已安装")).toBeTruthy();
expect(invoke).toHaveBeenCalledWith("list-installed", {
origin: "apm",
pkgnameList: undefined,
});
});
it("refreshes Spark installed state for favorites in both mode", async () => {
invoke.mockImplementation(async (channel: string, payload?: unknown) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "list-installed") {
const request = payload as { origin?: string };
if (request.origin === "spark") {
return {
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "spark",
},
],
};
}
return { success: true, apps: [] };
}
return [];
});
render(App);
await waitFor(() => {
expect(screen.getByText("2")).toBeTruthy();
});
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(await screen.findByText("已安装")).toBeTruthy();
expect(invoke).toHaveBeenCalledWith("list-installed", {
origin: "spark",
pkgnameList: ["wps"],
});
});
it("clears favorite data and leaves protected favorites view after logout", async () => {
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("我的收藏"));
expect(
await screen.findByRole("heading", { name: "我的收藏" }),
).toBeTruthy();
expect(await screen.findByText("默认收藏夹 (1)")).toBeTruthy();
expect(await screen.findByText("wps · office")).toBeTruthy();
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
await waitFor(() => {
expect(screen.getByRole("button", { name: "登录 / 注册" })).toBeTruthy();
});
expect(screen.queryByText("默认收藏夹 (1)")).toBeNull();
expect(screen.queryByText("wps · office")).toBeNull();
expect(screen.queryByRole("heading", { name: "我的收藏" })).toBeNull();
});
it("does not reopen the favorite selector when folder loading resolves after logout", async () => {
const slowFolders = createDeferred<FavoriteFolder[]>();
vi.mocked(listFavoriteFolders).mockReturnValueOnce(slowFolders.promise);
render(App);
await fireEvent.click(await screen.findByText("全部应用"));
await fireEvent.click(await screen.findByText("wps · 1.0.0"));
expect(await screen.findByRole("heading", { name: "WPS" })).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "收藏" }));
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
await waitFor(() => {
expect(screen.getByRole("button", { name: "登录 / 注册" })).toBeTruthy();
});
slowFolders.resolve([
{
id: 42,
name: "旧账号收藏夹",
itemCount: 1,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
]);
await slowFolders.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧账号收藏夹 (1)")).toBeNull();
expect(screen.queryByText("wps · office")).toBeNull();
expect(screen.queryByText("旧账号收藏夹")).toBeNull();
expect(screen.queryByRole("dialog", { name: "选择收藏夹" })).toBeNull();
});
it("ignores downloaded history that resolves after switching users", async () => {
const firstHistory = createDeferred<DownloadedAppList>();
const secondHistory = createDeferred<DownloadedAppList>();
vi.mocked(listDownloadedApps)
.mockReturnValueOnce(firstHistory.promise)
.mockReturnValueOnce(secondHistory.promise);
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
setSecondUserSession();
const secondUserButton = await screen.findByRole("button", {
name: /^Second User$/,
});
if (!screen.queryByText("用户管理")) {
await fireEvent.click(secondUserButton);
}
await fireEvent.click(await screen.findByText("用户管理"));
secondHistory.resolve(downloadedList([]));
const secondUserFrame = await screen.findByTitle("星火账号用户管理");
expect((secondUserFrame as HTMLIFrameElement).src).toContain("user=second");
firstHistory.resolve(
downloadedList([
{
id: 77,
appKey: "app:office:old-account-app",
pkgname: "old-account-app",
name: "旧账号应用",
category: "office",
selectedOrigin: "apm",
version: "1.0.0",
packageArch: "amd64",
downloadedAt: "2026-05-18T00:00:00Z",
},
]),
);
await firstHistory.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧账号应用")).toBeNull();
expect(screen.getByTitle("星火账号用户管理")).toBeTruthy();
});
it("ignores older downloaded history refreshes for the same user", async () => {
const firstHistory = createDeferred<DownloadedAppList>();
const secondHistory = createDeferred<DownloadedAppList>();
vi.mocked(listDownloadedApps)
.mockReturnValueOnce(firstHistory.promise)
.mockReturnValueOnce(secondHistory.promise);
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
await fireEvent.click(screen.getByRole("button", { name: "刷新" }));
secondHistory.resolve(
downloadedList([
{
id: 88,
appKey: "app:office:new-app",
pkgname: "new-app",
name: "新下载应用",
category: "office",
selectedOrigin: "apm",
version: "2.0.0",
packageArch: "amd64",
downloadedAt: "2026-05-18T00:00:00Z",
},
]),
);
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
firstHistory.resolve(
downloadedList([
{
id: 77,
appKey: "app:office:old-app",
pkgname: "old-app",
name: "旧下载应用",
category: "office",
selectedOrigin: "apm",
version: "1.0.0",
packageArch: "amd64",
downloadedAt: "2026-05-18T00:00:00Z",
},
]),
);
await firstHistory.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧下载应用")).toBeNull();
expect(screen.queryByText("新下载应用")).toBeNull();
});
it("ignores older favorite folder refreshes for the same user", async () => {
const firstFolders = createDeferred<FavoriteFolder[]>();
const secondFolders = createDeferred<FavoriteFolder[]>();
vi.mocked(listFavoriteFolders)
.mockReturnValueOnce(firstFolders.promise)
.mockReturnValueOnce(secondFolders.promise);
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByRole("button", { name: "我的收藏" }));
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByRole("button", { name: "我的收藏" })) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByRole("button", { name: "我的收藏" }));
secondFolders.resolve([
{
id: 42,
name: "新收藏夹",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
]);
expect(await screen.findByText("新收藏夹 (0)")).toBeTruthy();
firstFolders.resolve([
{
id: 41,
name: "旧收藏夹",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
]);
await firstFolders.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧收藏夹 (0)")).toBeNull();
expect(screen.getByText("新收藏夹 (0)")).toBeTruthy();
});
it("restores cloud apps by origin and package when category changed", async () => {
vi.mocked(fetchSyncedAppList).mockResolvedValueOnce({
snapshotName: "默认列表",
clientArch: "amd64",
distro: "deepin 25",
updatedAt: "2026-05-18T00:00:00Z",
items: [
{
pkgname: "wps",
origin: "apm",
category: "legacy-office",
version: "1.0.0",
packageArch: "amd64",
appName: "WPS Cloud",
iconUrl: "",
},
],
});
invoke.mockImplementation(async (channel: string, payload?: unknown) => {
if (channel === "get-store-filter") return "apm";
if (channel === "check-spark-available") return false;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "check-installed") return false;
if (channel === "list-installed") {
const request = payload as { origin?: string };
if (request.origin === "apm") return { success: true, apps: [] };
}
return [];
});
render(App);
await fireEvent.click(await screen.findByText("应用管理"));
await fireEvent.click(
await screen.findByRole("button", { name: /从账号恢复/ }),
);
await fireEvent.click(await screen.findByLabelText("WPS Cloud"));
await fireEvent.click(screen.getByRole("button", { name: "加入安装队列" }));
await waitFor(() => {
expect(window.ipcRenderer.send).toHaveBeenCalledWith(
"queue-install",
expect.stringContaining('"pkgname":"wps"'),
);
});
});
it("keeps the installed modal scoped to the active origin after sync", async () => {
invoke.mockImplementation(async (channel: string, payload?: unknown) => {
if (channel === "get-store-filter") return "both";
if (channel === "check-spark-available") return true;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "list-installed") {
const request = payload as { origin?: string };
if (request.origin === "spark") {
return {
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "spark",
},
],
};
}
return { success: true, apps: [] };
}
return [];
});
vi.mocked(uploadSyncedAppList).mockResolvedValueOnce(syncedList([]));
render(App);
await fireEvent.click(await screen.findByText("应用管理"));
const modal = screen.getByText("已安装应用").closest(".fixed");
if (!(modal instanceof HTMLElement)) throw new Error("modal not found");
expect(within(modal).getByText("暂无已安装应用")).toBeTruthy();
await fireEvent.click(
within(modal).getByRole("button", { name: /同步到账号/ }),
);
await waitFor(() => {
expect(uploadSyncedAppList).toHaveBeenCalled();
});
expect(within(modal).queryByText("WPS")).toBeNull();
expect(within(modal).getByText("暂无已安装应用")).toBeTruthy();
});
it("ignores overlapping installed sync requests", async () => {
const syncUpload = createDeferred<SyncedAppList>();
vi.mocked(uploadSyncedAppList).mockReturnValue(syncUpload.promise);
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "apm";
if (channel === "check-spark-available") return false;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "list-installed") return { success: true, apps: [] };
return [];
});
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
syncUpload.resolve(syncedList([]));
expect(uploadSyncedAppList).not.toHaveBeenCalled();
});
it("clears manual sync feedback before another user opens account management", async () => {
const syncUpload = createDeferred<SyncedAppList>();
vi.mocked(uploadSyncedAppList).mockReturnValue(syncUpload.promise);
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "apm";
if (channel === "check-spark-available") return false;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "list-installed") return { success: true, apps: [] };
return [];
});
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
syncUpload.resolve(syncedList([]));
expect(screen.queryByText("同步完成")).toBeNull();
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
await waitFor(() => {
expect(screen.getByRole("button", { name: "登录 / 注册" })).toBeTruthy();
});
setSecondUserSession();
const secondUserButton = await screen.findByRole("button", {
name: /^Second User$/,
});
if (!screen.queryByText("用户管理")) {
await fireEvent.click(secondUserButton);
}
await fireEvent.click(await screen.findByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
expect(screen.queryByText("同步完成")).toBeNull();
});
it("does not upload stale sync candidates after logout", async () => {
const slowInstalled = createDeferred<{
success: true;
apps: Array<{
pkgname: string;
name: string;
version: string;
arch: string;
flags: string;
origin: "apm";
}>;
}>();
let listInstalledCalls = 0;
invoke.mockImplementation(async (channel: string) => {
if (channel === "get-store-filter") return "apm";
if (channel === "check-spark-available") return false;
if (channel === "check-apm-available") return true;
if (channel === "get-app-version") return "5.0.0";
if (channel === "get-system-info") return { distro: "deepin 25" };
if (channel === "list-installed") {
listInstalledCalls += 1;
if (listInstalledCalls === 1) return slowInstalled.promise;
return { success: true, apps: [] };
}
return [];
});
vi.mocked(uploadSyncedAppList).mockResolvedValue(syncedList([]));
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByText("退出登录")) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByText("退出登录"));
slowInstalled.resolve({
success: true,
apps: [
{
pkgname: "wps",
name: "WPS",
version: "1.0.0",
arch: "amd64",
flags: "installed",
origin: "apm",
},
],
});
await slowInstalled.promise;
await Promise.resolve();
setSecondUserSession();
const secondUserButton = await screen.findByRole("button", {
name: /^Second User$/,
});
if (!screen.queryByText("用户管理")) {
await fireEvent.click(secondUserButton);
}
await fireEvent.click(await screen.findByText("用户管理"));
expect(await screen.findByTitle("星火账号用户管理")).toBeTruthy();
const uploadedItemNames = vi
.mocked(uploadSyncedAppList)
.mock.calls.flatMap(([payload]) =>
payload.items.map((item) => item.pkgname),
);
expect(uploadedItemNames).not.toContain("wps");
});
});