/* @vitest-environment jsdom */ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { AuthUser } from '../../../packages/shared/src/contracts/auth'; import * as hostBridgeServices from '../../services/host-bridge/hostBridge'; import { PlatformActiveProfileView } from './PlatformActiveProfileView'; const updateAuthProfileMock = vi.hoisted(() => vi.fn()); vi.mock('../../services/authService', () => ({ updateAuthProfile: updateAuthProfileMock, })); const callbacks = { onLogin: vi.fn(), onOpenApiKeys: vi.fn(), onOpenCommunity: vi.fn(), onOpenFeedback: vi.fn(), onOpenRecharge: vi.fn(), onOpenRewardCode: vi.fn(), onOpenSettings: vi.fn(), onOpenWalletLedger: vi.fn(), onUserUpdated: vi.fn(), }; const authenticatedUser: AuthUser = { id: 'user-1', publicUserCode: '100001', displayName: '测试玩家', avatarUrl: null, phoneNumberMasked: null, loginMethod: 'password' as const, bindingStatus: 'active', wechatBound: false, }; describe('PlatformActiveProfileView', () => { beforeEach(() => { Object.values(callbacks).forEach((callback) => callback.mockReset()); updateAuthProfileMock.mockReset(); }); it('shows the login entry for guests', () => { render( , ); expect(screen.getByRole('main', { name: '我的' })).toBeTruthy(); expect(screen.getByText('尚未登录')).toBeTruthy(); expect(screen.getByRole('button', { name: '登录' })).toBeTruthy(); }); it('renders active account, wallet, and settings capabilities', () => { render( , ); expect(screen.getByText('测试玩家')).toBeTruthy(); expect(screen.getByText(/陶泥号:\s*100001/u)).toBeTruthy(); expect(screen.getByRole('button', { name: '泥点余额 108' })).toBeTruthy(); expect(screen.getByRole('button', { name: /泥点充值/u })).toBeTruthy(); expect(screen.getByRole('button', { name: /兑换码/u })).toBeTruthy(); expect(screen.getByRole('button', { name: /玩家社区/u })).toBeTruthy(); expect(screen.getByRole('button', { name: /反馈与建议/u })).toBeTruthy(); expect(screen.getByRole('button', { name: /通用设置/u })).toBeTruthy(); expect( screen.getByRole('button', { name: /开发者 API Key/u }), ).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '用户协议' })); const legalDialog = screen.getByRole('dialog', { name: '用户协议' }); expect(legalDialog.parentElement?.className).toContain( 'platform-theme--light', ); }); it('edits the nickname through the profile identity action', async () => { updateAuthProfileMock.mockResolvedValueOnce({ ...authenticatedUser, displayName: '新昵称', }); render( , ); fireEvent.click(screen.getByRole('button', { name: '修改昵称' })); const input = screen.getByRole('textbox', { name: '新昵称' }); fireEvent.change(input, { target: { value: 'a' } }); fireEvent.click(screen.getByRole('button', { name: '保存' })); expect(screen.getByText('昵称需要 2 到 20 位')).toBeTruthy(); expect(updateAuthProfileMock).not.toHaveBeenCalled(); fireEvent.change(input, { target: { value: '新昵称' } }); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(updateAuthProfileMock).toHaveBeenCalledWith({ displayName: '新昵称', }); expect(callbacks.onUserUpdated).toHaveBeenCalledWith({ ...authenticatedUser, displayName: '新昵称', }); }); }); it('keeps avatar validation on the direct camera action', () => { render( , ); const avatarInput = screen .getAllByLabelText('上传头像') .find((element) => element instanceof HTMLInputElement); expect(avatarInput).toBeTruthy(); fireEvent.change(avatarInput as HTMLInputElement, { target: { files: [new File(['avatar'], 'avatar.txt', { type: 'text/plain' })], }, }); expect(screen.getByText('头像仅支持 jpg、png、webp')).toBeTruthy(); expect(updateAuthProfileMock).not.toHaveBeenCalled(); }); it('imports avatar images through the native HostBridge', async () => { class MockFileReader { result: string | null = null; onload: null | (() => void) = null; onerror: null | (() => void) = null; readAsDataURL() { this.result = 'data:image/webp;base64,YXZhdGFy'; this.onload?.(); } } class MockImage { onload: null | (() => void) = null; onerror: null | (() => void) = null; naturalWidth = 720; naturalHeight = 720; set src(_value: string) { this.onload?.(); } } vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader); vi.stubGlobal('Image', MockImage as unknown as typeof Image); const inputClickSpy = vi .spyOn(HTMLInputElement.prototype, 'click') .mockImplementation(() => undefined); vi.spyOn( hostBridgeServices, 'canUseNativeHostCapability', ).mockImplementation((capability) => capability === 'file.importImage'); vi.spyOn(hostBridgeServices, 'importHostImageFile').mockResolvedValue({ action: 'selected', fileName: 'native-avatar.webp', base64Data: 'YXZhdGFy', mimeType: 'image/webp', bytes: 6, }); try { render( , ); fireEvent.click(screen.getByRole('button', { name: '上传头像' })); await waitFor(() => { expect(screen.getByRole('dialog', { name: '裁剪头像' })).toBeTruthy(); }); expect(hostBridgeServices.importHostImageFile).toHaveBeenCalledTimes(1); expect(inputClickSpy).not.toHaveBeenCalled(); expect(screen.getByLabelText('头像裁剪操作区')).toBeTruthy(); } finally { vi.restoreAllMocks(); vi.unstubAllGlobals(); } }); });