From ebb4545ad6c1cbc0c3b604130c0e425ee9e8ec6f Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 11 Jun 2026 17:20:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A4=BC=E7=89=A9=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../games/pages/RoomRpsConfigPage.jsx | 41 +++++- .../games/pages/RoomRpsConfigPage.test.jsx | 127 ++++++++++++++++++ .../components/GiftSelectDrawer.test.jsx | 77 +++++++++++ src/features/resources/pages/GiftListPage.jsx | 3 +- 4 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 src/features/games/pages/RoomRpsConfigPage.test.jsx create mode 100644 src/features/resources/components/GiftSelectDrawer.test.jsx diff --git a/src/features/games/pages/RoomRpsConfigPage.jsx b/src/features/games/pages/RoomRpsConfigPage.jsx index 7d8bb4b..439a159 100644 --- a/src/features/games/pages/RoomRpsConfigPage.jsx +++ b/src/features/games/pages/RoomRpsConfigPage.jsx @@ -6,6 +6,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { PERMISSIONS } from "@/app/permissions"; import { useAuth } from "@/app/auth/AuthProvider.jsx"; import { getRoomRpsConfig, updateRoomRpsConfig } from "@/features/games/api"; +import { listGifts } from "@/features/resources/api"; +import { GiftSelectField } from "@/features/resources/components/GiftSelectDrawer.jsx"; import styles from "@/features/games/games.module.css"; import { AdminActionIconButton, @@ -47,6 +49,8 @@ export function RoomRpsConfigPage() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [editing, setEditing] = useState(false); + const [gifts, setGifts] = useState([]); + const [giftsLoading, setGiftsLoading] = useState(true); const load = useCallback(async () => { setLoading(true); @@ -60,9 +64,27 @@ export function RoomRpsConfigPage() { } }, [showToast]); + const loadGifts = useCallback(async () => { + setGiftsLoading(true); + try { + const result = await listGifts({ page_size: 200, status: "active" }); + setGifts(result.items || []); + } catch (err) { + showToast(err.message || "加载礼物列表失败", "error"); + } finally { + setGiftsLoading(false); + } + }, [showToast]); + useEffect(() => { load(); - }, [load]); + loadGifts(); + }, [load, loadGifts]); + + const refresh = useCallback(() => { + load(); + loadGifts(); + }, [load, loadGifts]); const rows = useMemo(() => (config ? [config] : []), [config]); const columns = useMemo( @@ -158,7 +180,7 @@ export function RoomRpsConfigPage() { + } @@ -173,6 +195,8 @@ export function RoomRpsConfigPage() { {form.stakeGifts.map((gift, index) => (
- setGiftTier(setForm, form, index, { giftId: event.target.value })} + onChange={(value) => setGiftTier(setForm, form, index, { giftId: value })} /> ({ + AuthProvider: ({ children }) => children, + useAuth: () => ({ can: () => true }), +})); + +vi.mock("@/shared/ui/SideDrawer.jsx", () => ({ + SideDrawer({ actions, children, drawerProps, onClose, open, title }) { + if (!open) { + return null; + } + return ( + + ); + }, +})); + +const patchBodies = []; + +beforeEach(() => { + patchBodies.length = 0; + vi.stubGlobal("fetch", vi.fn(mockFetch)); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("room rps gift tier uses shared gift drawer selection before saving", async () => { + const user = userEvent.setup(); + + render( + + + + + , + ); + + expect(await screen.findByText("房内猜拳")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "编辑" })); + expect(await screen.findByRole("dialog", { name: "房内猜拳配置" })).toBeInTheDocument(); + + await user.click(screen.getByDisplayValue("Rock Rose")); + + expect(screen.getByRole("dialog", { name: "选择第 1 档礼物" })).toHaveAttribute("data-z-index", "1600"); + await user.click(screen.getByText("Lucky Star")); + + expect(screen.getByDisplayValue("Lucky Star")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "保存配置" })); + + await waitFor(() => expect(patchBodies).toHaveLength(1)); + expect(patchBodies[0].stakeGifts[0]).toMatchObject({ + enabled: true, + giftId: 10005, + sortOrder: 1, + }); +}); + +async function mockFetch(input, init = {}) { + const url = String(input); + const method = init.method || "GET"; + + if (url.includes("/v1/admin/game/room-rps/config") && method === "GET") { + return jsonResponse({ + config: { + challengeTimeoutMs: 600000, + gameId: "room_rps", + revealCountdownMs: 3000, + stakeGifts: [ + { enabled: true, giftId: "10001", giftName: "Rock Rose", sortOrder: 1 }, + { enabled: true, giftId: "10002", giftName: "Paper Bell", sortOrder: 2 }, + { enabled: true, giftId: "10003", giftName: "Scissor Crown", sortOrder: 3 }, + { enabled: true, giftId: "10004", giftName: "Victory Cup", sortOrder: 4 }, + ], + status: "active", + }, + }); + } + + if (url.includes("/v1/admin/gifts")) { + return jsonResponse({ + items: [ + { coinPrice: 10, giftId: "10001", giftTypeCode: "normal", name: "Rock Rose", resourceId: 11 }, + { coinPrice: 20, giftId: "10005", giftTypeCode: "normal", name: "Lucky Star", resourceId: 12 }, + ], + page: 1, + pageSize: 200, + total: 2, + }); + } + + if (url.includes("/v1/admin/game/room-rps/config") && method === "PATCH") { + const body = JSON.parse(String(init.body || "{}")); + patchBodies.push(body); + return jsonResponse({ + config: { + ...body, + gameId: "room_rps", + stakeGifts: body.stakeGifts.map((gift) => ({ + ...gift, + giftId: String(gift.giftId), + })), + }, + }); + } + + return jsonResponse(null); +} + +function jsonResponse(data) { + return new Response(JSON.stringify({ code: 0, data }), { status: 200 }); +} diff --git a/src/features/resources/components/GiftSelectDrawer.test.jsx b/src/features/resources/components/GiftSelectDrawer.test.jsx new file mode 100644 index 0000000..da96b68 --- /dev/null +++ b/src/features/resources/components/GiftSelectDrawer.test.jsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { useState } from "react"; +import { expect, test, vi } from "vitest"; +import { GiftSelectField } from "./GiftSelectDrawer.jsx"; + +vi.mock("@/shared/ui/SideDrawer.jsx", () => ({ + SideDrawer({ children, drawerProps, onClose, open, title }) { + if (!open) { + return null; + } + return ( + + ); + }, +})); + +const gifts = [ + { + chargeAssetType: "COIN", + coinPrice: 10, + giftId: "10001", + giftTypeCode: "normal", + name: "Rose Gift", + resource: { previewUrl: "https://media.haiyihy.com/rose.png", resourceCode: "rose_resource" }, + resourceId: 11, + }, + { + chargeAssetType: "COIN", + coinPrice: 20, + giftId: "10002", + giftTypeCode: "normal", + name: "Lucky Star", + resource: { resourceCode: "lucky_star_resource" }, + resourceId: 12, + }, +]; + +test("gift select field opens right drawer and writes selected gift id", () => { + const changes = []; + + function Harness() { + const [value, setValue] = useState(""); + return ( + { + changes.push({ gift, value: nextValue }); + setValue(nextValue); + }} + /> + ); + } + + render(); + + fireEvent.click(screen.getByPlaceholderText("请选择礼物")); + + expect(screen.getByRole("dialog", { name: "选择礼物" })).toHaveAttribute("data-z-index", "1600"); + expect(screen.getByText("Rose Gift")).toBeInTheDocument(); + + fireEvent.change(screen.getByPlaceholderText("搜索礼物名称、礼物 ID、资源编码"), { + target: { value: "star" }, + }); + + expect(screen.queryByText("Rose Gift")).not.toBeInTheDocument(); + fireEvent.click(screen.getByText("Lucky Star")); + + expect(changes.at(-1)).toMatchObject({ value: "10002", gift: { giftId: "10002", name: "Lucky Star" } }); + expect(screen.getByDisplayValue("Lucky Star")).toBeInTheDocument(); + expect(screen.queryByRole("dialog", { name: "选择礼物" })).not.toBeInTheDocument(); +}); diff --git a/src/features/resources/pages/GiftListPage.jsx b/src/features/resources/pages/GiftListPage.jsx index 2136ae3..f1f526c 100644 --- a/src/features/resources/pages/GiftListPage.jsx +++ b/src/features/resources/pages/GiftListPage.jsx @@ -195,6 +195,7 @@ export function GiftListPage() { disabled={!page.abilities.canDeleteGift || items.length === 0 || deletingBatch} indeterminate={someChecked} size="small" + slotProps={{ input: { "aria-label": "选择当前页礼物" } }} onChange={(event) => toggleAllGifts(event.target.checked)} /> ), @@ -206,8 +207,8 @@ export function GiftListPage() { toggleGiftSelection(gift, event.target.checked)} /> );