import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, expect, test, vi } from "vitest"; import { ToastProvider } from "@/shared/ui/ToastProvider.jsx"; import { FinanceApp } from "./FinanceApp.jsx"; import { approveOperationsWithdrawalApplication, fetchFinanceOptions, fetchFinanceRechargeApps, fetchFinanceRechargeOverview, fetchFinanceRechargeSummaries, fetchFinanceSession, fetchFinanceWithdrawalOptions, listFinanceCoinSellerRechargeOrders, listFinanceRechargeDetails, listFinanceRechargeRegions, listFinanceWithdrawalApplications, listOperationsWithdrawalApplications, rejectOperationsWithdrawalApplication, } from "./api.js"; import { EMPTY_RECHARGE_OVERVIEW, EMPTY_RECHARGE_SUMMARY } from "./format.js"; vi.mock("./api.js", () => ({ approveFinanceWithdrawalApplication: vi.fn(), approveOperationsWithdrawalApplication: vi.fn(), createFinanceCoinSellerRechargeOrder: vi.fn(), exportFinanceRechargeBills: vi.fn(), fetchFinanceOptions: vi.fn(), fetchFinanceRechargeApps: vi.fn(), fetchFinanceRechargeOverview: vi.fn(), fetchFinanceRechargeSummaries: vi.fn(), fetchFinanceSession: vi.fn(), fetchFinanceWithdrawalOptions: vi.fn(), grantFinanceCoinSellerRechargeOrder: vi.fn(), getFinanceCoinSellerRechargeExchangeRate: vi.fn(), getFinanceUSDTAddresses: vi.fn(), listFinanceCoinSellerRechargeOrders: vi.fn(), listFinanceRechargeDetails: vi.fn(), listFinanceRechargeRegions: vi.fn(), listFinanceWithdrawalApplications: vi.fn(), listOperationsWithdrawalApplications: vi.fn(), rejectFinanceWithdrawalApplication: vi.fn(), rejectOperationsWithdrawalApplication: vi.fn(), refreshFinanceGoogleRechargePaid: vi.fn(), replaceFinanceCoinSellerRechargeExchangeRate: vi.fn(), upsertFinanceUSDTAddress: vi.fn(), quoteFinanceCoinSellerRechargeOrder: vi.fn(), verifyFinanceCoinSellerRechargeOrder: vi.fn(), verifyFinanceCoinSellerRechargeReceipt: vi.fn() })); const defaultSession = { canAccessFinance: true, canAuditOperationsWithdrawalApplications: false, canAuditWithdrawalApplications: false, canConfigureCoinSellerRechargeExchangeRate: false, canCreateCoinSellerRechargeOrder: false, canGrantCoinSellerRechargeOrder: false, canUpdateUSDTAddress: false, canVerifyCoinSellerRechargeOrder: false, canViewAppRechargeDetails: false, canViewCoinSellerRechargeOrders: false, canViewOperationsWithdrawalApplications: false, canViewWithdrawalApplications: true, permissions: ["finance-withdrawal:view"], user: { account: "admin", name: "admin" } }; beforeEach(() => { vi.mocked(fetchFinanceOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "lalu" }] }); vi.mocked(fetchFinanceRechargeApps).mockResolvedValue([{ appCode: "lalu", appName: "Lalu" }]); vi.mocked(fetchFinanceRechargeOverview).mockResolvedValue(EMPTY_RECHARGE_OVERVIEW); vi.mocked(fetchFinanceRechargeSummaries).mockResolvedValue({ merged: EMPTY_RECHARGE_SUMMARY, perApp: [] }); vi.mocked(fetchFinanceSession).mockResolvedValue(defaultSession); vi.mocked(fetchFinanceWithdrawalOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "lalu" }] }); vi.mocked(listFinanceCoinSellerRechargeOrders).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 }); vi.mocked(listFinanceRechargeDetails).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 }); vi.mocked(listFinanceRechargeRegions).mockResolvedValue([]); vi.mocked(listFinanceWithdrawalApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 }); vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 }); }); afterEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); window.history.pushState(null, "", "/"); }); test("preserves the finance entry when the expired session asks the user to log in again", async () => { vi.mocked(fetchFinanceSession).mockRejectedValueOnce(new Error("登录已失效")); window.history.pushState(null, "", "/finance/?view=rechargeDetails"); render( ); expect(await screen.findByRole("link", { name: "重新登录" })).toHaveAttribute( "href", "/login?redirect=%2Ffinance%2F%3Fview%3DrechargeDetails" ); }); test("defaults recharge finance views to yesterday in China timezone", async () => { vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 6, 8, 4)); vi.mocked(fetchFinanceSession).mockResolvedValue({ ...defaultSession, canViewAppRechargeDetails: true, permissions: ["finance-recharge-detail:view"] }); render( ); const yesterdayButton = await screen.findByRole("button", { name: "昨天" }); expect(yesterdayButton).toHaveClass("is-on"); expect(screen.getByRole("button", { name: "今天" })).toBeInTheDocument(); await waitFor(() => expect(listFinanceRechargeDetails).toHaveBeenCalled()); const [query] = vi.mocked(listFinanceRechargeDetails).mock.calls.at(-1); expect(query.start_at_ms).toBe(Date.UTC(2026, 6, 6, 16)); expect(query.end_at_ms).toBe(Date.UTC(2026, 6, 7, 16)); }); test("opens the permitted operations withdrawal deep link and synchronizes menu changes", async () => { vi.mocked(fetchFinanceSession).mockResolvedValue({ ...defaultSession, canAuditOperationsWithdrawalApplications: true, canViewOperationsWithdrawalApplications: true, permissions: ["finance-withdrawal:view", "operations-withdrawal:audit"] }); window.history.pushState(null, "", "/finance/?view=withdrawalOperations"); render( ); expect(await screen.findByRole("heading", { name: "用户提现运营审核" })).toBeInTheDocument(); await waitFor(() => expect(listOperationsWithdrawalApplications).toHaveBeenCalled()); fireEvent.click(screen.getByRole("button", { name: "用户提现财务审核" })); expect(window.location.search).toBe("?view=withdrawalFinance"); expect(screen.getByRole("heading", { name: "用户提现财务审核" })).toBeInTheDocument(); }); test("falls back from an unauthorized operations deep link to the finance review queue", async () => { window.history.pushState(null, "", "/finance/?view=withdrawalOperations"); render( ); expect(await screen.findByRole("heading", { name: "用户提现财务审核" })).toBeInTheDocument(); expect(window.location.search).toBe("?view=withdrawalFinance"); expect(screen.queryByRole("button", { name: "用户提现运营审核" })).not.toBeInTheDocument(); }); test("withdrawal app filter uses only the MoneyAccess scope catalog", async () => { vi.mocked(fetchFinanceOptions).mockResolvedValue({ apps: [ { appCode: "lalu", appName: "Lalu" }, { appCode: "aslan", appName: "Aslan" } ] }); vi.mocked(fetchFinanceWithdrawalOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "Lalu" }] }); render( ); const appFilter = await screen.findByRole("combobox", { name: "APP" }); fireEvent.mouseDown(appFilter); expect(await screen.findByRole("option", { name: "全部 APP" })).toBeInTheDocument(); expect(screen.getByRole("option", { name: "Lalu" })).toBeInTheDocument(); expect(screen.queryByRole("option", { name: "Aslan" })).not.toBeInTheDocument(); }); test("submits operations review with the withdrawal record app instead of the global app", async () => { vi.mocked(fetchFinanceSession).mockResolvedValue({ ...defaultSession, canAuditOperationsWithdrawalApplications: true, canViewOperationsWithdrawalApplications: true, canViewWithdrawalApplications: false, permissions: ["operations-withdrawal:audit"] }); vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({ items: [ { appCode: "aslan", createdAtMs: 1760000000000, id: 16, operationsStatus: "pending", status: "pending", userId: "10002", withdrawAddress: "TRX-operations", withdrawAmount: 20, withdrawMethod: "USDT-TRC20" } ], page: 1, pageSize: 50, total: 1 }); vi.mocked(approveOperationsWithdrawalApplication).mockResolvedValue({ id: 16 }); window.history.pushState(null, "", "/finance/?view=withdrawalOperations"); render( ); fireEvent.click(await screen.findByRole("button", { name: "通过" })); fireEvent.click(await screen.findByRole("button", { name: "确认" })); await waitFor(() => { expect(approveOperationsWithdrawalApplication).toHaveBeenCalledWith(16, "aslan", {}); }); }); test("retries a claimed operations rejection with its saved remark", async () => { vi.mocked(fetchFinanceSession).mockResolvedValue({ ...defaultSession, canAuditOperationsWithdrawalApplications: true, canViewOperationsWithdrawalApplications: true, canViewWithdrawalApplications: false, permissions: ["operations-withdrawal:audit"] }); vi.mocked(listOperationsWithdrawalApplications).mockResolvedValue({ items: [ { appCode: "aslan", createdAtMs: 1760000000000, id: 17, operationsAuditRemark: "命中运营风控", operationsStatus: "rejecting", status: "pending", userId: "10003", withdrawAddress: "TRX-rejecting", withdrawAmount: 30, withdrawMethod: "USDT-TRC20" } ], page: 1, pageSize: 50, total: 1 }); vi.mocked(rejectOperationsWithdrawalApplication).mockResolvedValue({ id: 17 }); window.history.pushState(null, "", "/finance/?view=withdrawalOperations"); render( ); expect(await screen.findByText("拒绝处理中")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "通过" })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "重试拒绝" })); const savedRemark = await screen.findByRole("textbox", { name: "审核备注" }); expect(savedRemark).toHaveValue("命中运营风控"); expect(savedRemark).toBeDisabled(); fireEvent.click(screen.getByRole("button", { name: "确认" })); await waitFor(() => { expect(rejectOperationsWithdrawalApplication).toHaveBeenCalledWith(17, "aslan", { auditRemark: "命中运营风控" }); }); expect(approveOperationsWithdrawalApplication).not.toHaveBeenCalled(); });