mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
fix(account): record downloads after success
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import App from "@/App.vue";
|
||||
import { recordDownloadedApp } from "@/modules/backendApi";
|
||||
import { setAuthSession } from "@/global/authState";
|
||||
import type { DownloadResult } from "@/global/typedefinition";
|
||||
|
||||
const invoke = vi.fn();
|
||||
const send = vi.fn();
|
||||
const ipcHandlers = new Map<string, (...args: unknown[]) => void>();
|
||||
|
||||
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: "Office suite",
|
||||
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(),
|
||||
exchangeFlarumToken: vi.fn(),
|
||||
listFavoriteFolders: vi.fn(async () => []),
|
||||
listFavoriteItems: vi.fn(async () => []),
|
||||
recordDownloadedApp: vi.fn(async () => undefined),
|
||||
setBackendToken: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("App download records", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ipcHandlers.clear();
|
||||
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: [] };
|
||||
if (channel === "check-installed") return false;
|
||||
return [];
|
||||
});
|
||||
|
||||
Object.assign(window.ipcRenderer, {
|
||||
invoke,
|
||||
send,
|
||||
on: vi.fn((channel: string, handler: (...args: unknown[]) => void) => {
|
||||
ipcHandlers.set(channel, handler);
|
||||
}),
|
||||
off: 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",
|
||||
forumLevel: "管理员",
|
||||
forumGroups: ["管理员"],
|
||||
},
|
||||
});
|
||||
|
||||
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("records a download only after the queued install completes successfully", async () => {
|
||||
render(App);
|
||||
|
||||
await fireEvent.click(
|
||||
await screen.findByRole("button", { name: "全部应用 1" }),
|
||||
);
|
||||
await fireEvent.click(await screen.findByText("WPS"));
|
||||
await fireEvent.click(await screen.findByRole("button", { name: "安装" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(send).toHaveBeenCalledWith(
|
||||
"queue-install",
|
||||
expect.stringContaining('"pkgname":"wps"'),
|
||||
);
|
||||
});
|
||||
expect(recordDownloadedApp).not.toHaveBeenCalled();
|
||||
|
||||
const queuedPayload = vi
|
||||
.mocked(send)
|
||||
.mock.calls.find(
|
||||
([channel]) => channel === "queue-install",
|
||||
)?.[1] as string;
|
||||
const queuedDownload = JSON.parse(queuedPayload) as { id: number };
|
||||
const completion: DownloadResult = {
|
||||
id: queuedDownload.id,
|
||||
time: Date.now(),
|
||||
message: "installed",
|
||||
success: true,
|
||||
exitCode: 0,
|
||||
status: "completed",
|
||||
origin: "apm",
|
||||
};
|
||||
|
||||
ipcHandlers.get("install-complete")?.({}, completion);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(recordDownloadedApp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
appKey: "app:office:wps",
|
||||
pkgname: "wps",
|
||||
selectedOrigin: "apm",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,17 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import AppDetailPage from "@/components/AppDetailPage.vue";
|
||||
import type { App } from "@/global/typedefinition";
|
||||
import type { App, ReviewTags } from "@/global/typedefinition";
|
||||
|
||||
vi.mock("@/components/ReviewsPanel.vue", () => ({
|
||||
default: {
|
||||
name: "ReviewsPanel",
|
||||
props: ["appKey", "tags", "loggedIn"],
|
||||
template:
|
||||
'<div data-testid="reviews-panel" :data-app-key="appKey" :data-origin="tags.origin" :data-version="tags.version"></div>',
|
||||
},
|
||||
}));
|
||||
|
||||
const app: App = {
|
||||
name: "WPS",
|
||||
@@ -24,6 +33,40 @@ const app: App = {
|
||||
currentStatus: "not-installed",
|
||||
};
|
||||
|
||||
const sparkApp: App = {
|
||||
...app,
|
||||
name: "WPS Spark",
|
||||
version: "2.0.0",
|
||||
filename: "wps_2.0.0_amd64.deb",
|
||||
origin: "spark",
|
||||
};
|
||||
|
||||
const apmApp: App = {
|
||||
...app,
|
||||
name: "WPS APM",
|
||||
version: "1.0.0",
|
||||
filename: "wps_1.0.0_amd64.deb",
|
||||
origin: "apm",
|
||||
};
|
||||
|
||||
const mergedApp: App = {
|
||||
...sparkApp,
|
||||
isMerged: true,
|
||||
sparkApp,
|
||||
apmApp,
|
||||
viewingOrigin: "spark",
|
||||
};
|
||||
|
||||
const sparkTags: ReviewTags = {
|
||||
origin: "spark",
|
||||
category: "office",
|
||||
pkgname: "wps",
|
||||
version: "2.0.0",
|
||||
packageArch: "amd64",
|
||||
clientArch: "amd64",
|
||||
distro: "deepin 25",
|
||||
};
|
||||
|
||||
describe("AppDetailPage", () => {
|
||||
it("renders as page, emits back, and gates favorite for anonymous users", async () => {
|
||||
const rendered = render(AppDetailPage, {
|
||||
@@ -47,4 +90,42 @@ describe("AppDetailPage", () => {
|
||||
"收藏应用需要登录星火账号。",
|
||||
);
|
||||
});
|
||||
|
||||
it("updates review identity when switching a merged app origin", async () => {
|
||||
render(AppDetailPage, {
|
||||
props: {
|
||||
app: mergedApp,
|
||||
screenshots: [],
|
||||
sparkInstalled: false,
|
||||
apmInstalled: false,
|
||||
loggedIn: false,
|
||||
reviewAppKey: "spark:amd64-store:office:wps",
|
||||
reviewTags: sparkTags,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
|
||||
"data-app-key",
|
||||
"spark:amd64-store:office:wps",
|
||||
);
|
||||
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
|
||||
"data-origin",
|
||||
"spark",
|
||||
);
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "APM" }));
|
||||
|
||||
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
|
||||
"data-app-key",
|
||||
"apm:amd64-apm:office:wps",
|
||||
);
|
||||
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
|
||||
"data-origin",
|
||||
"apm",
|
||||
);
|
||||
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
|
||||
"data-version",
|
||||
"1.0.0",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
import { render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ReviewsPanel from "@/components/ReviewsPanel.vue";
|
||||
import type { ReviewTags } from "@/global/typedefinition";
|
||||
import {
|
||||
fetchRatingSummary,
|
||||
fetchReviews,
|
||||
submitReview,
|
||||
} from "@/modules/backendApi";
|
||||
import type {
|
||||
AppReview,
|
||||
RatingSummary,
|
||||
ReviewTags,
|
||||
} from "@/global/typedefinition";
|
||||
|
||||
const emptySummary: RatingSummary = {
|
||||
averageRating: 0,
|
||||
reviewCount: 0,
|
||||
starCounts: {},
|
||||
};
|
||||
|
||||
vi.mock("@/modules/backendApi", () => ({
|
||||
fetchRatingSummary: vi.fn(async () => emptySummary),
|
||||
fetchReviews: vi.fn(async () => []),
|
||||
submitReview: vi.fn(),
|
||||
}));
|
||||
|
||||
const tags: ReviewTags = {
|
||||
origin: "apm",
|
||||
@@ -15,6 +36,14 @@ const tags: ReviewTags = {
|
||||
};
|
||||
|
||||
describe("ReviewsPanel", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchRatingSummary).mockReset();
|
||||
vi.mocked(fetchReviews).mockReset();
|
||||
vi.mocked(submitReview).mockReset();
|
||||
vi.mocked(fetchRatingSummary).mockResolvedValue(emptySummary);
|
||||
vi.mocked(fetchReviews).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("shows anonymous login prompt and read-only review tags", () => {
|
||||
render(ReviewsPanel, {
|
||||
props: { appKey: "apm:amd64-apm:office:wps", tags, loggedIn: false },
|
||||
@@ -24,4 +53,88 @@ describe("ReviewsPanel", () => {
|
||||
expect(screen.getByText("1.0.0")).toBeTruthy();
|
||||
expect(screen.getByText("deepin 25")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("ignores stale review responses after app key changes", async () => {
|
||||
let resolveFirstSummary!: (summary: RatingSummary) => void;
|
||||
let resolveFirstReviews!: (reviews: AppReview[]) => void;
|
||||
let resolveSecondSummary!: (summary: RatingSummary) => void;
|
||||
let resolveSecondReviews!: (reviews: AppReview[]) => void;
|
||||
|
||||
vi.mocked(fetchRatingSummary)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFirstSummary = resolve;
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveSecondSummary = resolve;
|
||||
}),
|
||||
);
|
||||
vi.mocked(fetchReviews)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFirstReviews = resolve;
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveSecondReviews = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const rendered = render(ReviewsPanel, {
|
||||
props: { appKey: "first", tags, loggedIn: false },
|
||||
});
|
||||
|
||||
await rendered.rerender({ appKey: "second", tags, loggedIn: false });
|
||||
|
||||
resolveSecondSummary({ averageRating: 5, reviewCount: 1, starCounts: {} });
|
||||
resolveSecondReviews([
|
||||
{
|
||||
id: 2,
|
||||
rating: 5,
|
||||
content: "second review",
|
||||
version: tags.version,
|
||||
packageArch: tags.packageArch,
|
||||
clientArch: tags.clientArch,
|
||||
distro: tags.distro,
|
||||
origin: tags.origin,
|
||||
category: tags.category,
|
||||
createdAt: "2026-05-18T00:00:00Z",
|
||||
updatedAt: "2026-05-18T00:00:00Z",
|
||||
userDisplayName: "Second User",
|
||||
userAvatarUrl: "",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(await screen.findByText("second review")).toBeTruthy();
|
||||
expect(screen.getByText("5.0 / 5 (1)")).toBeTruthy();
|
||||
|
||||
resolveFirstSummary({ averageRating: 1, reviewCount: 1, starCounts: {} });
|
||||
resolveFirstReviews([
|
||||
{
|
||||
id: 1,
|
||||
rating: 1,
|
||||
content: "first review",
|
||||
version: tags.version,
|
||||
packageArch: tags.packageArch,
|
||||
clientArch: tags.clientArch,
|
||||
distro: tags.distro,
|
||||
origin: tags.origin,
|
||||
category: tags.category,
|
||||
createdAt: "2026-05-18T00:00:00Z",
|
||||
updatedAt: "2026-05-18T00:00:00Z",
|
||||
userDisplayName: "First User",
|
||||
userAvatarUrl: "",
|
||||
},
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(screen.getByText("second review")).toBeTruthy();
|
||||
expect(screen.getByText("5.0 / 5 (1)")).toBeTruthy();
|
||||
expect(screen.queryByText("first review")).toBeNull();
|
||||
expect(screen.queryByText("1.0 / 5 (1)")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user