hyapp-admin-platform/src/features/operations/pages/CoinAdjustmentPage.test.jsx
2026-07-05 13:45:04 +08:00

179 lines
6.6 KiB
JavaScript

import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { afterEach, expect, test, vi } from "vitest";
import { AppProviders } from "@/app/providers.jsx";
import { queryClient } from "@/shared/query/client";
import { CoinAdjustmentPage } from "./CoinAdjustmentPage.jsx";
vi.mock("@/app/auth/AuthProvider.jsx", () => ({
AuthProvider: ({ children }) => children,
useAuth: () => ({ can: () => true }),
}));
afterEach(() => {
queryClient.clear();
vi.unstubAllGlobals();
});
test("coin adjustment does not submit when remark is blank", async () => {
const user = userEvent.setup();
vi.stubGlobal("fetch", createCoinAdjustmentFetch());
renderPage();
await openReadyDialog(user);
await user.type(screen.getByRole("textbox", { name: "用户 ID" }), "1001");
await user.click(screen.getByRole("button", { name: "搜索" }));
await waitFor(() => expect(screen.getByText("测试用户")).toBeInTheDocument());
await user.type(screen.getByRole("textbox", { name: "备注" }), " ");
await user.type(screen.getByRole("textbox", { name: "发放数量" }), "500");
await user.click(screen.getByRole("button", { name: "确认" }));
await waitFor(() => expect(screen.getByText("请输入备注")).toBeInTheDocument());
expect(coinAdjustmentPostBodies()).toHaveLength(0);
});
test("coin adjustment request body includes trimmed remark", async () => {
const user = userEvent.setup();
vi.stubGlobal("fetch", createCoinAdjustmentFetch());
renderPage();
await openReadyDialog(user);
await user.type(screen.getByRole("textbox", { name: "用户 ID" }), "1001");
await user.click(screen.getByRole("button", { name: "搜索" }));
await waitFor(() => expect(screen.getByText("测试用户")).toBeInTheDocument());
await user.type(screen.getByRole("textbox", { name: "备注" }), " 客服复核后补发 ");
await user.type(screen.getByRole("textbox", { name: "发放数量" }), "500");
await user.click(screen.getByRole("button", { name: "确认" }));
await waitFor(() =>
expect(coinAdjustmentPostBodies()).toEqual([
expect.objectContaining({
amount: 500,
reason: "运营发放",
remark: "客服复核后补发",
targetUserId: "1001",
}),
]),
);
});
test("coin adjustment does not submit when remark exceeds 512 characters", async () => {
const user = userEvent.setup();
vi.stubGlobal("fetch", createCoinAdjustmentFetch());
renderPage();
await openReadyDialog(user);
await user.type(screen.getByRole("textbox", { name: "用户 ID" }), "1001");
await user.click(screen.getByRole("button", { name: "搜索" }));
await waitFor(() => expect(screen.getByText("测试用户")).toBeInTheDocument());
fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补".repeat(513) } });
await user.type(screen.getByRole("textbox", { name: "发放数量" }), "500");
await user.click(screen.getByRole("button", { name: "确认" }));
await waitFor(() => expect(screen.getByText("备注不能超过 512 个字符")).toBeInTheDocument());
expect(coinAdjustmentPostBodies()).toHaveLength(0);
});
test("coin adjustment list displays remark and falls back for empty remark", async () => {
vi.stubGlobal(
"fetch",
createCoinAdjustmentFetch({
items: [
coinAdjustmentFixture({ entryId: 1, remark: "客服复核后补发", transactionId: "tx-remark" }),
coinAdjustmentFixture({ entryId: 2, remark: "", transactionId: "tx-empty" }),
],
}),
);
renderPage();
await waitFor(() => expect(screen.getByText("客服复核后补发")).toBeInTheDocument());
expect(screen.getByText("备注")).toBeInTheDocument();
const rows = document.querySelectorAll(".admin-row:not(.admin-row--head)");
expect(within(rows[1]).getByText("-")).toBeInTheDocument();
});
function renderPage() {
return render(
<AppProviders>
<MemoryRouter>
<CoinAdjustmentPage />
</MemoryRouter>
</AppProviders>,
);
}
async function openReadyDialog(user) {
await waitFor(() => expect(screen.getByText("当前无数据")).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "金币增减" }));
await waitFor(() => expect(screen.getByRole("dialog", { name: "金币增减" })).toBeInTheDocument());
}
function coinAdjustmentPostBodies() {
return vi
.mocked(fetch)
.mock.calls.filter(([url, init]) => String(url).includes("/api/v1/admin/operations/coin-adjustments") && init?.method === "POST")
.map(([, init]) => JSON.parse(String(init?.body)));
}
function createCoinAdjustmentFetch({ items = [] } = {}) {
return vi.fn(async (input, init = {}) => {
const url = String(input);
const method = init.method || "GET";
if (url.includes("/api/v1/admin/operations/coin-adjustments/target")) {
return jsonResponse({
code: 0,
data: {
displayUserId: "1001",
userId: "1001",
username: "测试用户",
},
});
}
if (url.includes("/api/v1/admin/operations/coin-adjustments") && method === "POST") {
return jsonResponse({
code: 0,
data: {
amount: 500,
availableDelta: 500,
balanceAfter: 1500,
transactionId: "tx-created",
user: { displayUserId: "1001", userId: "1001", username: "测试用户" },
},
});
}
if (url.includes("/api/v1/admin/operations/coin-adjustments")) {
return jsonResponse({ code: 0, data: { items, page: 1, pageSize: 50, total: items.length } });
}
return jsonResponse({ code: 0, data: {} });
});
}
function coinAdjustmentFixture(patch = {}) {
return {
amount: 500,
availableAfter: 1500,
availableDelta: 500,
createdAtMs: 1762300800000,
direction: "income",
entryId: 1,
operator: { name: "运营" },
reason: "运营发放",
remark: "",
transactionId: "tx-1",
user: { displayUserId: "1001", userId: "1001", username: "测试用户" },
userId: "1001",
...patch,
};
}
function jsonResponse(body) {
return new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
status: 200,
});
}