173 lines
12 KiB
TypeScript
173 lines
12 KiB
TypeScript
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<CloudAgentAvatar>({ icon: '/old.png', icon_url: 'https://images.test/old.png' });
|
|
const [uploading, setUploading] = useState(false);
|
|
return <CloudAvatarEditor name="写作搭档" value={value} disabled={false} uploading={uploading} onUploading={setUploading} onChange={setValue} />;
|
|
}
|
|
|
|
it('shows a default avatar for absent or broken images and recovers when the image changes', () => {
|
|
const view = render(<CloudAvatar name="写作搭档" />);
|
|
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(<CloudAvatar name="写作搭档" src="https://images.test/broken.png" />);
|
|
fireEvent.error(screen.getByRole('img', { name: '写作搭档头像' }));
|
|
expect(screen.getByRole('img', { name: '写作搭档默认头像' })).toBeVisible();
|
|
view.rerender(<CloudAvatar name="写作搭档" src="https://images.test/new.png" />);
|
|
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(<CloudAvatar name="" />);
|
|
const portrait = screen.getByRole('img').getAttribute('src');
|
|
for (const name of ['学', '学习', '学习 Friend', '学习 Friend 🚀', '']) {
|
|
view.rerender(<CloudAvatar name={name} />);
|
|
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(<CloudAvatar identity="english-practice" name="英语陪练" />);
|
|
const portrait = screen.getByRole('img').getAttribute('src');
|
|
for (const name of ['英', '英语', '英语 Friend', '英语 Friend 🌟']) {
|
|
view.rerender(<CloudAvatar identity="english-practice" name={name} />);
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', portrait);
|
|
}
|
|
view.unmount();
|
|
const reopened = render(<CloudAvatar identity="english-practice" name="英语 Friend 🌟" src="https://images.test/saved.png" />);
|
|
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(<CloudAvatar identity="english-practice" name="新名字" src="https://images.test/replaced.png" />);
|
|
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(<CloudAvatar name="" />);
|
|
const portrait = screen.getByRole('img').getAttribute('src');
|
|
creation.rerender(<CloudAvatar name="我的英语陪练" />);
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', portrait);
|
|
creation.unmount();
|
|
const view = render(<CloudAvatar identity="assistant-a" name="同名助手" />);
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', portrait);
|
|
view.rerender(<CloudAvatar identity="assistant-b" name="同名助手" />);
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', portrait);
|
|
view.rerender(<CloudAvatar identity="assistant-a" name="改名后的助手" />);
|
|
expect(screen.getByRole('img')).toHaveAttribute('src', portrait);
|
|
view.unmount();
|
|
render(<CloudAvatar identity="assistant-a" name="改名后的助手" />);
|
|
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<CloudAgentAvatar>({});
|
|
const [uploading, setUploading] = useState(false);
|
|
return <><input aria-label="助手名称" value={name} onChange={event => setName(event.target.value)} />
|
|
<CloudAvatarEditor identity="english-practice" name={name} value={value} disabled={false} uploading={uploading} onUploading={setUploading} onChange={setValue} /></>;
|
|
}
|
|
render(<RenameEditor />);
|
|
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(<Editor />);
|
|
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(<Editor />);
|
|
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(<CloudAvatarEditor name="写作搭档" value={{}} disabled={false} uploading={false} onChange={changed} onUploading={vi.fn()} />);
|
|
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(<Editor />);
|
|
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(<CloudAvatarEditor name="写作搭档" value={{}} disabled={false} uploading={false} onChange={changed} onUploading={busy} />);
|
|
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
|
|
expect(busy).toHaveBeenCalledWith(true);
|
|
view.rerender(<CloudAvatarEditor name="写作搭档" value={{}} disabled={false} uploading onChange={changed} onUploading={busy} />);
|
|
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();
|
|
});
|