/* @vitest-environment jsdom */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, expect, test, vi } from 'vitest'; import { AuthUiContext } from '../auth/AuthUiContext'; import { RpgEntryHomeView, type RpgEntryHomeViewProps, } from './RpgEntryHomeView'; import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation'; const { mockGetRpgProfileWalletLedger } = vi.hoisted(() => ({ mockGetRpgProfileWalletLedger: vi.fn(async () => ({ entries: [ { id: 'ledger-1', amountDelta: -1, balanceAfter: 29, sourceType: 'asset_operation_consume', createdAt: '2026-04-28T10:00:00Z', }, { id: 'ledger-2', amountDelta: 30, balanceAfter: 30, sourceType: 'invite_invitee_reward', createdAt: '2026-04-28T09:00:00Z', }, ], })), })); vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({ getRpgProfileWalletLedger: mockGetRpgProfileWalletLedger, getRpgProfileRechargeCenter: vi.fn(async () => ({ walletBalance: 0, membership: { status: 'normal', tier: 'normal', startedAt: null, expiresAt: null, updatedAt: null, }, pointProducts: [ { productId: 'points_60', title: '60叙世币', priceCents: 600, kind: 'points', pointsAmount: 60, bonusPoints: 60, durationDays: 0, badgeLabel: '首充双倍', description: '首充送60叙世币', tier: 'normal', }, ], membershipProducts: [ { productId: 'member_month', title: '月卡', priceCents: 2800, kind: 'membership', pointsAmount: 0, bonusPoints: 0, durationDays: 30, badgeLabel: '', description: '30天会员', tier: 'month', }, ], benefits: [ { benefitName: '免叙世币回合数', normalValue: '30', monthValue: '100', seasonValue: '100', yearValue: '100', }, ], latestOrder: null, hasPointsRecharged: false, })), createRpgProfileRechargeOrder: vi.fn(async () => ({ order: { orderId: 'order-1', productId: 'points_60', productTitle: '60叙世币', kind: 'points', amountCents: 600, status: 'paid', paymentChannel: 'mock', paidAt: '2026-04-25T10:00:00Z', createdAt: '2026-04-25T10:00:00Z', pointsDelta: 120, membershipExpiresAt: null, }, center: { walletBalance: 120, membership: { status: 'normal', tier: 'normal', startedAt: null, expiresAt: null, updatedAt: null, }, pointProducts: [], membershipProducts: [], benefits: [], latestOrder: null, hasPointsRecharged: true, }, })), })); vi.mock('../ResolvedAssetImage', () => ({ ResolvedAssetImage: () => null, })); const originalMatchMedia = window.matchMedia; const puzzlePublicEntry = { sourceType: 'puzzle', workId: 'puzzle-work-public-1', profileId: 'puzzle-profile-public-1', publicWorkCode: 'PZ-EPUBLIC1', ownerUserId: 'user-2', authorDisplayName: '拼图玩家', worldName: '奇幻拼图', subtitle: '拼图关卡', summaryText: '一张用于公开分享的拼图作品。', coverImageSrc: null, themeTags: ['奇幻'], likeCount: 12, visibility: 'published', publishedAt: '1777110165.990127Z', updatedAt: '2026-04-25T10:00:00.000Z', } satisfies PlatformPublicGalleryCard; function mockDesktopLayout() { Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: vi.fn().mockImplementation(() => ({ matches: true, media: '(min-width: 1024px)', onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), })), }); } function renderProfileView(onRechargeSuccess = vi.fn()) { return render( action(), openSettingsModal: vi.fn(), openAccountModal: vi.fn(), logout: vi.fn(async () => undefined), musicVolume: 0.42, setMusicVolume: vi.fn(), platformTheme: 'light', setPlatformTheme: vi.fn(), isHydratingSettings: false, isPersistingSettings: false, settingsError: null, }} > , ); } function renderLoggedOutHomeView( openLoginModal = vi.fn(), overrides: Partial< Pick< RpgEntryHomeViewProps, | 'featuredEntries' | 'latestEntries' | 'onOpenGalleryDetail' | 'onSearchPublicCode' > > = {}, ) { return render( undefined), musicVolume: 0.42, setMusicVolume: vi.fn(), platformTheme: 'light', setPlatformTheme: vi.fn(), isHydratingSettings: false, isPersistingSettings: false, settingsError: null, }} > , ); } afterEach(() => { vi.clearAllMocks(); Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: originalMatchMedia, }); Object.defineProperty(navigator, 'clipboard', { configurable: true, value: undefined, }); }); test('opens wallet ledger modal from narrative coin card', async () => { const user = userEvent.setup(); renderProfileView(); await user.click(screen.getByText('剩余叙世币')); expect(await screen.findByText('叙世币账单')).toBeTruthy(); expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1); expect(screen.getByText('资产操作消耗')).toBeTruthy(); expect(screen.getByText('-1')).toBeTruthy(); expect(screen.getByText('填写邀请码奖励')).toBeTruthy(); expect(screen.getByText('+30')).toBeTruthy(); }); test('wallet ledger modal shows empty and error states', async () => { const user = userEvent.setup(); mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] }); renderProfileView(); await user.click(screen.getByText('剩余叙世币')); expect(await screen.findByText('暂无账单记录')).toBeTruthy(); await user.click(screen.getByLabelText('关闭叙世币账单')); mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败')); await user.click(screen.getByText('剩余叙世币')); expect(await screen.findByText('加载失败')).toBeTruthy(); expect(screen.getByText('重新加载')).toBeTruthy(); }); test('shows a reachable login entry in logged out mobile shell', async () => { const user = userEvent.setup(); const openLoginModal = vi.fn(); renderLoggedOutHomeView(openLoginModal); await user.click(screen.getByRole('button', { name: '登录' })); expect(openLoginModal).toHaveBeenCalledTimes(1); }); test('mobile home search submits public work code', async () => { const user = userEvent.setup(); const onSearchPublicCode = vi.fn(); render( undefined), musicVolume: 0.42, setMusicVolume: vi.fn(), platformTheme: 'light', setPlatformTheme: vi.fn(), isHydratingSettings: false, isPersistingSettings: false, settingsError: null, }} > , ); const searchInput = screen.getByPlaceholderText( '输入 SY / CW / BF / PZ 编号', ); await user.type(searchInput, 'PZ-PROFILE1{enter}'); expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1'); }); test('public gallery cards hide work code until detail is opened', async () => { const user = userEvent.setup(); const onOpenGalleryDetail = vi.fn(); renderLoggedOutHomeView(vi.fn(), { latestEntries: [puzzlePublicEntry], onOpenGalleryDetail, }); expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull(); expect( screen.queryByRole('button', { name: '复制作品号 PZ-EPUBLIC1' }), ).toBeNull(); await user.click(screen.getByRole('button', { name: /奇幻拼图/u })); expect(onOpenGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry); }); test('desktop trending list shows kind instead of work code or timestamp text', () => { mockDesktopLayout(); renderLoggedOutHomeView(vi.fn(), { latestEntries: [puzzlePublicEntry], }); expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull(); expect(screen.getAllByText('拼图').length).toBeGreaterThan(0); expect(screen.queryByText('1777110165.990127Z')).toBeNull(); });