import { useState } from 'react'; import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { CloudAvatar, CloudAvatarEditor } from '@/pages/CloudAgents/CloudAvatar'; import type { CloudAgentAvatar } from '../../shared/cloud-agents'; const api = vi.hoisted(() => ({ uploadAvatar: vi.fn(), useAvatarPreset: vi.fn() })); vi.mock('@/lib/cloud-agents-api', () => ({ cloudAgentsApi: api })); beforeEach(() => vi.resetAllMocks()); afterEach(cleanup); function Editor() { const [value, setValue] = useState({ icon: '/old.png', icon_url: 'https://images.test/old.png' }); const [uploading, setUploading] = useState(false); return ; } it('shows a default avatar for absent or broken images and recovers when the image changes', () => { const view = render(); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible(); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toHaveAttribute('src', expect.stringContaining('avatar-')); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toHaveStyle({ imageRendering: 'pixelated' }); view.rerender(); fireEvent.error(screen.getByRole('img', { name: '写作搭档头像' })); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible(); view.rerender(); expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/new.png'); }); it('keeps a new assistant portrait while its name is typed or cleared', () => { const view = render(); const portrait = screen.getByRole('img').getAttribute('src'); for (const name of ['学', '学习', '学习 Friend', '学习 Friend 🚀', '']) { view.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); expect(screen.getByRole('img')).toHaveAttribute('alt', `${name}默认头像`); } }); it('keeps an assistant portrait across renaming, reopening and a failed custom image', () => { const view = render(); const portrait = screen.getByRole('img').getAttribute('src'); for (const name of ['英', '英语', '英语 Friend', '英语 Friend 🌟']) { view.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); } view.unmount(); const reopened = render(); expect(screen.getByRole('img')).toHaveAttribute('src', 'https://images.test/saved.png'); fireEvent.error(screen.getByRole('img')); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); reopened.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', 'https://images.test/replaced.png'); }); it('uses one default portrait across creation, persisted identities and reopening', () => { const creation = render(); const portrait = screen.getByRole('img').getAttribute('src'); creation.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); creation.unmount(); const view = render(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); view.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); view.rerender(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); view.unmount(); render(); expect(screen.getByRole('img')).toHaveAttribute('src', portrait); }); it('keeps the default selection and chosen portrait stable while the name is edited', async () => { api.useAvatarPreset.mockResolvedValue({ icon: '/chosen.png', icon_url: 'https://images.test/chosen.png' }); function RenameEditor() { const [name, setName] = useState('英语陪练'); const [value, setValue] = useState({}); const [uploading, setUploading] = useState(false); return <> setName(event.target.value)} /> ; } render(); const portraits = () => screen.getAllByRole('button', { name: /^像素伙伴 / }); const initialSelection = portraits().find(button => button.getAttribute('aria-pressed') === 'true')!; const portrait = screen.getByRole('img', { name: '英语陪练默认头像' }).getAttribute('src'); expect(initialSelection.querySelector('img')).toHaveAttribute('src', portrait); for (const name of ['英', '英语', '英语 Friend']) { fireEvent.change(screen.getByRole('textbox', { name: '助手名称' }), { target: { value: name } }); expect(screen.getByRole('img', { name: `${name}默认头像` })).toHaveAttribute('src', portrait); expect(initialSelection).toHaveAttribute('aria-pressed', 'true'); expect(portraits().filter(button => button.getAttribute('aria-pressed') === 'true')).toHaveLength(1); } fireEvent.click(screen.getByRole('button', { name: '像素伙伴 7' })); await waitFor(() => expect(screen.getByRole('img', { name: '英语 Friend头像' })).toHaveAttribute('src', 'https://images.test/chosen.png')); fireEvent.change(screen.getByRole('textbox', { name: '助手名称' }), { target: { value: '新的伙伴' } }); expect(screen.getByRole('img', { name: '新的伙伴头像' })).toHaveAttribute('src', 'https://images.test/chosen.png'); expect(screen.getByRole('button', { name: '像素伙伴 7' })).toHaveAttribute('aria-pressed', 'true'); fireEvent.click(screen.getByRole('button', { name: '移除头像' })); expect(screen.getByRole('img', { name: '新的伙伴默认头像' })).toHaveAttribute('src', portrait); expect(initialSelection).toHaveAttribute('aria-pressed', 'true'); expect(api.useAvatarPreset).toHaveBeenCalledExactlyOnceWith('avatar-07'); }); it('offers sixteen platform portraits and uses the chosen portrait without a file picker', async () => { api.useAvatarPreset.mockResolvedValue({ icon: '/chosen.png', icon_url: 'https://images.test/chosen.png' }); render(); expect(screen.getAllByRole('button', { name: /^像素伙伴 / })).toHaveLength(16); fireEvent.click(screen.getByRole('button', { name: '像素伙伴 7' })); await waitFor(() => expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/chosen.png')); expect(api.useAvatarPreset).toHaveBeenCalledExactlyOnceWith('avatar-07'); expect(api.uploadAvatar).not.toHaveBeenCalled(); expect(screen.getByRole('button', { name: '像素伙伴 7' })).toHaveAttribute('aria-pressed', 'true'); fireEvent.click(screen.getByRole('button', { name: '移除头像' })); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible(); expect(screen.getAllByRole('button', { name: /^像素伙伴 / }).filter(button => button.getAttribute('aria-pressed') === 'true')).toHaveLength(1); }); it('preserves the previous avatar on preset failure and locks all choices while a selection is pending', async () => { let reject!: (reason: Error) => void; api.useAvatarPreset.mockReturnValue(new Promise((_resolve, failure) => { reject = failure; })); render(); fireEvent.click(screen.getByRole('button', { name: '像素伙伴 2' })); expect(screen.getByRole('button', { name: '正在使用…' })).toBeDisabled(); expect(screen.getAllByRole('button', { name: /^像素伙伴 / }).every(button => (button as HTMLButtonElement).disabled)).toBe(true); fireEvent.click(screen.getByRole('button', { name: '像素伙伴 3' })); await act(async () => reject(new Error('平台头像暂不可用'))); expect(screen.getByRole('alert')).toHaveTextContent('平台头像暂不可用'); expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/old.png'); expect(api.useAvatarPreset).toHaveBeenCalledExactlyOnceWith('avatar-02'); expect(screen.getByRole('button', { name: '像素伙伴 3' })).toBeEnabled(); }); it('drops a pending preset result after leaving the editor or account', async () => { let resolve!: (value: CloudAgentAvatar) => void; api.useAvatarPreset.mockReturnValue(new Promise(r => { resolve = r; })); const changed = vi.fn(); const view = render(); fireEvent.click(screen.getByRole('button', { name: '像素伙伴 4' })); view.unmount(); await act(async () => resolve({ icon: '/other-account.png', icon_url: 'https://images.test/other.png' })); expect(changed).not.toHaveBeenCalled(); }); it('retains the avatar after cancellation or failure, then supports replacing and removing it', async () => { api.uploadAvatar.mockResolvedValueOnce(null).mockRejectedValueOnce(new Error('上传失败')) .mockResolvedValueOnce({ icon: '/new.png', icon_url: 'https://images.test/new.png' }); render(); fireEvent.click(screen.getByRole('button', { name: '更换头像' })); await waitFor(() => expect(screen.getByRole('button', { name: '更换头像' })).toBeEnabled()); expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/old.png'); fireEvent.click(screen.getByRole('button', { name: '更换头像' })); expect(await screen.findByRole('alert')).toHaveTextContent('上传失败'); expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/old.png'); fireEvent.click(screen.getByRole('button', { name: '更换头像' })); await waitFor(() => expect(screen.getByRole('img', { name: '写作搭档头像' })).toHaveAttribute('src', 'https://images.test/new.png')); fireEvent.click(screen.getByRole('button', { name: '移除头像' })); expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible(); }); it('disables edits during upload and drops results after leaving the account or editor', async () => { let resolve!: (value: CloudAgentAvatar) => void; api.uploadAvatar.mockReturnValue(new Promise(r => { resolve = r; })); const changed = vi.fn(), busy = vi.fn(); const view = render(); fireEvent.click(screen.getByRole('button', { name: '上传头像' })); expect(busy).toHaveBeenCalledWith(true); view.rerender(); expect(screen.getByRole('button', { name: '正在上传…' })).toBeDisabled(); view.unmount(); await act(async () => resolve({ icon: '/other-account.png', icon_url: 'https://images.test/other.png' })); expect(changed).not.toHaveBeenCalled(); });