/* @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();
}
});
});