179 lines
6.6 KiB
JavaScript
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,
|
|
});
|
|
}
|