mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
feat(sync): add installed app cloud sync
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import AppListRestoreModal from "@/components/AppListRestoreModal.vue";
|
||||
import type { SyncedAppListItem } from "@/global/typedefinition";
|
||||
|
||||
const createItem = (
|
||||
overrides: Partial<SyncedAppListItem> = {},
|
||||
): SyncedAppListItem => ({
|
||||
pkgname: "spark-notes",
|
||||
origin: "spark",
|
||||
category: "office",
|
||||
version: "1.0.0",
|
||||
packageArch: "amd64",
|
||||
appName: "Spark Notes",
|
||||
iconUrl: "",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("AppListRestoreModal", () => {
|
||||
it("emits selected installable cloud items", async () => {
|
||||
const rendered = render(AppListRestoreModal, {
|
||||
props: {
|
||||
show: true,
|
||||
loading: false,
|
||||
error: "",
|
||||
items: [
|
||||
createItem(),
|
||||
createItem({ pkgname: "amber-ce", appName: "Amber CE" }),
|
||||
],
|
||||
installedKeys: new Set<string>(),
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByLabelText("Spark Notes"));
|
||||
await fireEvent.click(screen.getByRole("button", { name: "加入安装队列" }));
|
||||
|
||||
expect(rendered.emitted("install-selected")?.[0]?.[0]).toEqual([
|
||||
expect.objectContaining({ pkgname: "spark-notes" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("disables already installed cloud items", () => {
|
||||
render(AppListRestoreModal, {
|
||||
props: {
|
||||
show: true,
|
||||
loading: false,
|
||||
error: "",
|
||||
items: [createItem()],
|
||||
installedKeys: new Set(["spark:spark-notes"]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText("Spark Notes")).toBeDisabled();
|
||||
expect(screen.getByText("已安装")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,8 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,6 +59,8 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,6 +79,8 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,6 +103,8 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -119,6 +127,8 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -136,9 +146,75 @@ describe("InstalledAppsModal", () => {
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.queryByRole("button", { name: "查看详情" })).toBeNull();
|
||||
});
|
||||
|
||||
it("requests login for cloud actions when logged out", async () => {
|
||||
const rendered = render(InstalledAppsModal, {
|
||||
props: {
|
||||
show: true,
|
||||
apps: [],
|
||||
loading: false,
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: false,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "同步到账号" }));
|
||||
await fireEvent.click(screen.getByRole("button", { name: "从账号恢复" }));
|
||||
|
||||
expect(rendered.emitted("request-login")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("emits cloud sync and restore events when logged in", async () => {
|
||||
const rendered = render(InstalledAppsModal, {
|
||||
props: {
|
||||
show: true,
|
||||
apps: [],
|
||||
loading: false,
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: true,
|
||||
syncing: false,
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "同步到账号" }));
|
||||
await fireEvent.click(screen.getByRole("button", { name: "从账号恢复" }));
|
||||
|
||||
expect(rendered.emitted("sync-to-account")).toHaveLength(1);
|
||||
expect(rendered.emitted("restore-from-account")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("disables sync button while syncing", () => {
|
||||
render(InstalledAppsModal, {
|
||||
props: {
|
||||
show: true,
|
||||
apps: [],
|
||||
loading: false,
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
loggedIn: true,
|
||||
syncing: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole("button", { name: "同步中" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildSyncItems, cloudItemKey } from "@/modules/appListSync";
|
||||
import type { App } from "@/global/typedefinition";
|
||||
|
||||
const createApp = (overrides: Partial<App> = {}): App => ({
|
||||
name: "Spark Notes",
|
||||
pkgname: "spark-notes",
|
||||
version: "1.0.0",
|
||||
filename: "spark-notes_1.0.0_amd64.deb",
|
||||
torrent_address: "",
|
||||
author: "",
|
||||
contributor: "",
|
||||
website: "",
|
||||
update: "",
|
||||
size: "1 MB",
|
||||
more: "",
|
||||
tags: "",
|
||||
img_urls: [],
|
||||
icons: "https://example.test/icon.png",
|
||||
category: "office",
|
||||
origin: "spark",
|
||||
currentStatus: "installed",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("appListSync", () => {
|
||||
it("builds cloud sync items for installed store-recognized user apps", () => {
|
||||
expect(buildSyncItems([createApp()])).toEqual([
|
||||
{
|
||||
pkgname: "spark-notes",
|
||||
origin: "spark",
|
||||
category: "office",
|
||||
version: "1.0.0",
|
||||
packageArch: "amd64",
|
||||
appName: "Spark Notes",
|
||||
iconUrl: "https://example.test/icon.png",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("filters out non-installed unknown dependency and unusable package entries", () => {
|
||||
const items = buildSyncItems([
|
||||
createApp({ pkgname: "not-installed", currentStatus: "not-installed" }),
|
||||
createApp({ pkgname: "unknown-app", category: "unknown" }),
|
||||
createApp({ pkgname: "dependency", isDependency: true }),
|
||||
createApp({ pkgname: "" }),
|
||||
createApp({ pkgname: "blank-origin", origin: "spark" }),
|
||||
createApp({ pkgname: "kept", origin: "apm", arch: "arm64" }),
|
||||
]);
|
||||
|
||||
expect(items).toEqual([
|
||||
expect.objectContaining({ pkgname: "blank-origin" }),
|
||||
expect.objectContaining({ pkgname: "kept", packageArch: "arm64" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses pkgname as appName and blank icon when optional display fields are missing", () => {
|
||||
const app = createApp({ icons: "", pkgname: "fallback-name" });
|
||||
app.name = "";
|
||||
const [syncItem] = buildSyncItems([app]);
|
||||
|
||||
expect(syncItem).toMatchObject({
|
||||
appName: "fallback-name",
|
||||
iconUrl: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds stable installed keys from origin and package", () => {
|
||||
expect(cloudItemKey({ origin: "apm", pkgname: "amber-ce" })).toBe(
|
||||
"apm:amber-ce",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user