fix(reviews): restore modal detail review gating

This commit is contained in:
2026-05-19 12:22:52 +08:00
parent 04b0ca061b
commit fd17fc127d
9 changed files with 456 additions and 71 deletions
+154
View File
@@ -0,0 +1,154 @@
import { fireEvent, render, screen } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AppDetailModal from "@/components/AppDetailModal.vue";
import type { App, ReviewTags } from "@/global/typedefinition";
vi.mock("axios", () => ({
default: {
get: vi.fn(async () => ({ status: 200, data: "42" })),
},
}));
vi.mock("@/components/ReviewsPanel.vue", () => ({
default: {
name: "ReviewsPanel",
props: ["appKey", "tags", "loggedIn", "canSubmit"],
template:
'<div data-testid="reviews-panel" :data-app-key="appKey" :data-origin="tags.origin" :data-version="tags.version" :data-can-submit="String(canSubmit)"></div>',
},
}));
const app: 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",
};
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",
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("AppDetailModal", () => {
beforeEach(() => {
window.apm_store.arch = "amd64";
});
it("renders detail content inside a popup-style modal overlay", () => {
const { container } = render(AppDetailModal, {
attrs: { "data-app-modal": "detail" },
props: {
show: true,
app,
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
loggedIn: false,
reviewAppKey: "apm:amd64-apm:office:wps",
reviewTags: sparkTags,
},
});
const overlay = container.querySelector('[data-app-modal="detail"]');
expect(overlay).toBeTruthy();
expect(overlay?.className).toContain("fixed");
expect(overlay?.querySelector(".modal-panel")).toBeTruthy();
});
it("updates review identity when switching a merged app origin", async () => {
render(AppDetailModal, {
props: {
show: true,
app: mergedApp,
screenshots: [],
sparkInstalled: true,
apmInstalled: true,
loggedIn: true,
reviewAppKey: "spark:amd64-store:office:wps",
reviewTags: sparkTags,
},
});
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
"data-app-key",
"spark:amd64-store:office:wps",
);
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",
);
});
it("marks reviews read-only when the selected origin is not installed", () => {
render(AppDetailModal, {
props: {
show: true,
app,
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
loggedIn: true,
reviewAppKey: "apm:amd64-apm:office:wps",
reviewTags: sparkTags,
},
});
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
"data-can-submit",
"false",
);
});
});
+23 -4
View File
@@ -7,9 +7,9 @@ import type { App, ReviewTags } from "@/global/typedefinition";
vi.mock("@/components/ReviewsPanel.vue", () => ({
default: {
name: "ReviewsPanel",
props: ["appKey", "tags", "loggedIn"],
props: ["appKey", "tags", "loggedIn", "canSubmit"],
template:
'<div data-testid="reviews-panel" :data-app-key="appKey" :data-origin="tags.origin" :data-version="tags.version"></div>',
'<div data-testid="reviews-panel" :data-app-key="appKey" :data-origin="tags.origin" :data-version="tags.version" :data-can-submit="String(canSubmit)"></div>',
},
}));
@@ -118,8 +118,8 @@ describe("AppDetailPage", () => {
props: {
app: mergedApp,
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
sparkInstalled: true,
apmInstalled: true,
loggedIn: true,
reviewAppKey: "spark:amd64-store:office:wps",
reviewTags: sparkTags,
@@ -150,4 +150,23 @@ describe("AppDetailPage", () => {
"1.0.0",
);
});
it("marks reviews read-only when the selected origin is not installed", () => {
render(AppDetailPage, {
props: {
app,
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
loggedIn: true,
reviewAppKey: "apm:amd64-apm:office:wps",
reviewTags: sparkTags,
},
});
expect(screen.getByTestId("reviews-panel")).toHaveAttribute(
"data-can-submit",
"false",
);
});
});
+32 -1
View File
@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/vue";
import { fireEvent, render, screen } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ReviewsPanel from "@/components/ReviewsPanel.vue";
@@ -56,6 +56,20 @@ describe("ReviewsPanel", () => {
expect(fetchReviews).not.toHaveBeenCalled();
});
it("hides the submit form when reviews are read-only", () => {
render(ReviewsPanel, {
props: {
appKey: "apm:amd64-apm:office:wps",
tags,
loggedIn: true,
canSubmit: false,
},
});
expect(screen.queryByRole("button", { name: "发表评论" })).toBeNull();
expect(screen.getByText("安装应用后可发表评论。")).toBeTruthy();
});
it("ignores stale review responses after app key changes", async () => {
let resolveFirstSummary!: (summary: RatingSummary) => void;
let resolveFirstReviews!: (reviews: AppReview[]) => void;
@@ -139,4 +153,21 @@ describe("ReviewsPanel", () => {
expect(screen.queryByText("first review")).toBeNull();
expect(screen.queryByText("1.0 / 5 (1)")).toBeNull();
});
it("shows a friendly submit error instead of raw network errors", async () => {
vi.mocked(submitReview).mockRejectedValueOnce(new Error("Network Error"));
render(ReviewsPanel, {
props: { appKey: "apm:amd64-apm:office:wps", tags, loggedIn: true },
});
await fireEvent.update(
screen.getByPlaceholderText("分享你的使用体验"),
"好用",
);
await fireEvent.click(screen.getByRole("button", { name: "发表评论" }));
expect(
await screen.findByText("无法连接星火账号服务,请稍后重试。"),
).toBeTruthy();
});
});
+36 -6
View File
@@ -1,7 +1,7 @@
import axios from "axios";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { exchangeFlarumToken } from "@/modules/backendApi";
import { exchangeFlarumToken, submitReview } from "@/modules/backendApi";
const axiosMocks = vi.hoisted(() => {
const post = vi.fn();
@@ -57,18 +57,48 @@ describe("backend API auth exchange", () => {
},
"Spark backend auth exchange failed",
);
expect(JSON.stringify(loggerMocks.error.mock.calls)).not.toContain("forum-token");
expect(JSON.stringify(loggerMocks.error.mock.calls)).not.toContain(
"forum-token",
);
});
it("maps backend server failures to an update-required login error", async () => {
const error = Object.assign(new Error("Request failed with status code 500"), {
isAxiosError: true,
response: { status: 500 },
});
const error = Object.assign(
new Error("Request failed with status code 500"),
{
isAxiosError: true,
response: { status: 500 },
},
);
axiosMocks.post.mockRejectedValue(error);
await expect(
exchangeFlarumToken({ flarumUserId: "42", flarumToken: "forum-token" }),
).rejects.toThrow("星火账号服务异常,请确认后端数据库迁移已执行后重试。");
});
it("maps review submission connection failures to a friendly error", async () => {
const error = Object.assign(new Error("Network Error"), {
code: "ERR_NETWORK",
isAxiosError: true,
request: {},
});
axiosMocks.post.mockRejectedValue(error);
await expect(
submitReview("apm:amd64-apm:office:wps", {
rating: 5,
content: "好用",
tags: {
origin: "apm",
category: "office",
pkgname: "wps",
version: "1.0.0",
packageArch: "amd64",
clientArch: "amd64",
distro: "deepin 25",
},
}),
).rejects.toThrow("无法连接星火账号服务,请稍后重试。");
});
});