mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
fix(account): polish sidebar favorites and sync feedback
This commit is contained in:
@@ -678,12 +678,64 @@ describe("App account placeholders", () => {
|
||||
await fireEvent.click(
|
||||
await screen.findByRole("button", { name: "立即同步" }),
|
||||
);
|
||||
await fireEvent.click(screen.getByRole("button", { name: "立即同步" }));
|
||||
expect(screen.getByRole("button", { name: "同步中..." })).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(uploadSyncedAppList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
syncUpload.resolve(syncedList([]));
|
||||
expect(await screen.findByText("同步完成")).toBeTruthy();
|
||||
});
|
||||
|
||||
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("用户管理"));
|
||||
await fireEvent.click(
|
||||
await screen.findByRole("button", { name: "立即同步" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(uploadSyncedAppList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
syncUpload.resolve(syncedList([]));
|
||||
expect(await screen.findByText("同步完成")).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();
|
||||
});
|
||||
|
||||
setSecondUserSession();
|
||||
const secondUserButton = await screen.findByRole("button", {
|
||||
name: /^Second User$/,
|
||||
});
|
||||
if (!screen.queryByText("用户管理")) {
|
||||
await fireEvent.click(secondUserButton);
|
||||
}
|
||||
await fireEvent.click(await screen.findByText("用户管理"));
|
||||
|
||||
expect(screen.queryByText("同步完成")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not upload stale sync candidates after logout", async () => {
|
||||
|
||||
@@ -55,6 +55,22 @@ describe("AppListRestoreModal", () => {
|
||||
expect(screen.getByText("已安装")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("treats the same package installed from another source as installed", () => {
|
||||
render(AppListRestoreModal, {
|
||||
props: {
|
||||
show: true,
|
||||
loading: false,
|
||||
error: "",
|
||||
items: [createItem({ origin: "spark" })],
|
||||
installedKeys: new Set(["apm:spark-notes"]),
|
||||
installedPackageKeys: new Set(["spark-notes"]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText("Spark Notes")).toBeDisabled();
|
||||
expect(screen.getByText("已安装")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("removes selected items when they become installed", async () => {
|
||||
const rendered = render(AppListRestoreModal, {
|
||||
props: {
|
||||
@@ -72,4 +88,26 @@ describe("AppListRestoreModal", () => {
|
||||
expect(screen.getByLabelText("Spark Notes")).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "加入安装队列" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("removes selected items when the same package becomes installed from another source", async () => {
|
||||
const rendered = render(AppListRestoreModal, {
|
||||
props: {
|
||||
show: true,
|
||||
loading: false,
|
||||
error: "",
|
||||
items: [createItem({ origin: "spark" })],
|
||||
installedKeys: new Set<string>(),
|
||||
installedPackageKeys: new Set<string>(),
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByLabelText("Spark Notes"));
|
||||
await rendered.rerender({
|
||||
installedPackageKeys: new Set(["spark-notes"]),
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText("Spark Notes")).toBeDisabled();
|
||||
expect(screen.getByLabelText("Spark Notes")).not.toBeChecked();
|
||||
expect(screen.getByRole("button", { name: "加入安装队列" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,4 +45,39 @@ describe("AppSidebar account entry", () => {
|
||||
expect(screen.getByText("我的收藏")).toBeTruthy();
|
||||
expect(screen.getByText("退出登录")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps long account names inside the sidebar account entry", () => {
|
||||
const longUser: SparkUser = {
|
||||
...user,
|
||||
displayName: "SuperEndermanSMSuperEndermanSMSuperEndermanSM",
|
||||
};
|
||||
|
||||
const { container } = render(AppSidebar, {
|
||||
props: { ...baseProps, currentUser: longUser },
|
||||
});
|
||||
|
||||
const accountButton = screen.getByRole("button", {
|
||||
name: /SuperEndermanSM/,
|
||||
});
|
||||
const textWrapper = accountButton.querySelector(
|
||||
"[data-testid='account-text']",
|
||||
);
|
||||
const accountName = screen.getByText(longUser.displayName);
|
||||
|
||||
expect(textWrapper?.className).toContain("min-w-0");
|
||||
expect(accountName.className).toContain("truncate");
|
||||
expect(container.textContent).toContain(longUser.displayName);
|
||||
});
|
||||
|
||||
it("closes the quick menu after selecting an account action", async () => {
|
||||
const rendered = render(AppSidebar, {
|
||||
props: { ...baseProps, currentUser: user },
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: /Momen/ }));
|
||||
await fireEvent.click(screen.getByRole("button", { name: "用户管理" }));
|
||||
|
||||
expect(rendered.emitted("open-user-management")).toHaveLength(1);
|
||||
expect(screen.queryByRole("button", { name: "用户管理" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const categoryBarSource = readFileSync(
|
||||
resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../../components/CategoryBar.vue",
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
describe("CategoryBar", () => {
|
||||
it("uses the requested blue for the selected category pill", () => {
|
||||
expect(categoryBarSource.toLowerCase()).toContain("background: #2b7fff;");
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import FavoriteFolderManager from "@/components/FavoriteFolderManager.vue";
|
||||
import type {
|
||||
App,
|
||||
FavoriteFolder,
|
||||
ResolvedFavoriteItem,
|
||||
} from "@/global/typedefinition";
|
||||
@@ -30,6 +31,26 @@ const item: ResolvedFavoriteItem = {
|
||||
selectedApp: null,
|
||||
};
|
||||
|
||||
const selectedApp: App = {
|
||||
name: "WPS",
|
||||
pkgname: "wps",
|
||||
version: "1.0.0",
|
||||
filename: "wps_1.0.0_amd64.deb",
|
||||
torrent_address: "",
|
||||
author: "",
|
||||
contributor: "",
|
||||
website: "",
|
||||
update: "",
|
||||
size: "110M",
|
||||
more: "Office suite",
|
||||
tags: "office",
|
||||
img_urls: [],
|
||||
icons: "",
|
||||
category: "office",
|
||||
origin: "apm",
|
||||
currentStatus: "not-installed",
|
||||
};
|
||||
|
||||
describe("FavoriteFolderManager", () => {
|
||||
it("shows downlisted favorites and emits bulk delete", async () => {
|
||||
const rendered = render(FavoriteFolderManager, {
|
||||
@@ -48,4 +69,23 @@ describe("FavoriteFolderManager", () => {
|
||||
|
||||
expect(rendered.emitted("remove-selected")?.[0]?.[0]).toEqual([2]);
|
||||
});
|
||||
|
||||
it("opens a favorite item's app detail from the row content", async () => {
|
||||
const rendered = render(FavoriteFolderManager, {
|
||||
props: {
|
||||
folders: [folder],
|
||||
activeFolderId: 1,
|
||||
items: [{ ...item, status: "installable", selectedApp }],
|
||||
loading: false,
|
||||
error: "",
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", { name: "打开 WPS 详情" }),
|
||||
);
|
||||
|
||||
expect(rendered.emitted("open-detail")?.[0]?.[0]).toEqual(selectedApp);
|
||||
expect(rendered.emitted("remove-selected")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { render } from "@testing-library/vue";
|
||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import FavoriteFolderSelector from "@/components/FavoriteFolderSelector.vue";
|
||||
import type { FavoriteFolder } from "@/global/typedefinition";
|
||||
|
||||
const defaultFolder: FavoriteFolder = {
|
||||
id: 1,
|
||||
name: "默认收藏夹",
|
||||
itemCount: 1,
|
||||
createdAt: "2026-05-18T00:00:00Z",
|
||||
updatedAt: "2026-05-18T00:00:00Z",
|
||||
};
|
||||
|
||||
describe("FavoriteFolderSelector", () => {
|
||||
it("renders above the app detail modal and its child popups", () => {
|
||||
@@ -16,4 +25,30 @@ describe("FavoriteFolderSelector", () => {
|
||||
|
||||
expect(overlay?.className).toContain("z-[90]");
|
||||
});
|
||||
|
||||
it("does not duplicate the default folder returned by the backend", () => {
|
||||
render(FavoriteFolderSelector, {
|
||||
props: {
|
||||
show: true,
|
||||
folders: [defaultFolder],
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole("button", { name: "默认收藏夹" })).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("offers creating a folder while selecting favorites", async () => {
|
||||
const rendered = render(FavoriteFolderSelector, {
|
||||
props: {
|
||||
show: true,
|
||||
folders: [defaultFolder],
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "新建收藏夹" }));
|
||||
|
||||
expect(rendered.emitted("create-folder")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,14 @@ import { describe, expect, it } from "vitest";
|
||||
import LoginModal from "@/components/LoginModal.vue";
|
||||
|
||||
describe("LoginModal", () => {
|
||||
it("does not show the old password forwarding note", () => {
|
||||
render(LoginModal, {
|
||||
props: { show: true, loading: false, error: "" },
|
||||
});
|
||||
|
||||
expect(screen.queryByText(/密码仅直接/)).toBeNull();
|
||||
});
|
||||
|
||||
it("emits login credentials and register request", async () => {
|
||||
const rendered = render(LoginModal, {
|
||||
props: { show: true, loading: false, error: "" },
|
||||
|
||||
@@ -35,6 +35,8 @@ describe("UserManagementView", () => {
|
||||
syncEnabled: true,
|
||||
loading: false,
|
||||
error: "",
|
||||
syncing: false,
|
||||
syncMessage: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,4 +47,24 @@ describe("UserManagementView", () => {
|
||||
expect(screen.getByText("WPS")).toBeTruthy();
|
||||
expect(screen.getByLabelText("自动同步已安装应用")).toBeChecked();
|
||||
});
|
||||
|
||||
it("shows manual sync progress and result feedback", async () => {
|
||||
const { rerender } = render(UserManagementView, {
|
||||
props: {
|
||||
user,
|
||||
downloadedApps: [],
|
||||
syncEnabled: false,
|
||||
loading: false,
|
||||
error: "",
|
||||
syncing: true,
|
||||
syncMessage: "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole("button", { name: "同步中..." })).toBeDisabled();
|
||||
|
||||
await rerender({ syncing: false, syncMessage: "同步完成" });
|
||||
|
||||
expect(screen.getByText("同步完成")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildSyncItems,
|
||||
cloudItemKey,
|
||||
cloudPackageKey,
|
||||
mergeInstalledApps,
|
||||
} from "@/modules/appListSync";
|
||||
import type { App } from "@/global/typedefinition";
|
||||
@@ -76,6 +77,10 @@ describe("appListSync", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("builds origin-agnostic package keys for cross-source restore detection", () => {
|
||||
expect(cloudPackageKey({ pkgname: "amber-ce" })).toBe("amber-ce");
|
||||
});
|
||||
|
||||
it("merges refreshed apps without mutating active modal origin lists", () => {
|
||||
const current = [createApp({ origin: "apm", pkgname: "apm-installed" })];
|
||||
const refreshed = [
|
||||
|
||||
Reference in New Issue
Block a user