2703acda84
ConversationModelSelect 在已保存选择失效时回退到后台默认模型,只要默认模型可用就不会出现「选择模型」空态 首页模型选择器改为挂载即加载目录,触发按钮直接落在默认模型上(移除懒加载) 首页不再需要 lazy 路径,清理组件中已无调用方的懒加载逻辑 同步更新模型选择相关回归测试与首页/运行时配置用例
140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
import {
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import { resolveTauriInvoke } from '../src/app/tauri';
|
|
import { ConversationModelSelect } from '../src/features/project-workspace/ConversationModelSelect';
|
|
import { loadClientLlmModels } from '../src/services/clientApi';
|
|
|
|
vi.mock('../src/app/tauri', () => ({ resolveTauriInvoke: vi.fn() }));
|
|
vi.mock('../src/services/clientApi', () => ({ loadClientLlmModels: vi.fn() }));
|
|
const invoke = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(resolveTauriInvoke).mockReturnValue(invoke);
|
|
vi.mocked(loadClientLlmModels).mockResolvedValue({
|
|
defaultModelId: 'quality',
|
|
models: [
|
|
{ id: 'quality', displayName: '高质量' },
|
|
{ id: 'fast', displayName: '快速' },
|
|
],
|
|
});
|
|
invoke.mockImplementation(async (command, input) => ({
|
|
config: {
|
|
selectedModelId:
|
|
command === 'select_game_creator_model' ? input.modelId : 'quality',
|
|
},
|
|
}));
|
|
});
|
|
afterEach(cleanup);
|
|
|
|
test('only displays aliases and persists selection through the native command', async () => {
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByRole('button', { name: '对话模型' });
|
|
expect(screen.queryByText('gpt-6-astra')).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
fireEvent.click(screen.getByRole('option', { name: '快速' }));
|
|
await waitFor(() =>
|
|
expect(invoke).toHaveBeenCalledWith('select_game_creator_model', {
|
|
modelId: 'fast',
|
|
}),
|
|
);
|
|
await waitFor(() => expect(onReady).toHaveBeenLastCalledWith(true));
|
|
expect(
|
|
screen.getByRole('button', { name: '对话模型' }).textContent,
|
|
).toContain('快速');
|
|
});
|
|
|
|
test('falls back to the default model when the saved selection was removed', async () => {
|
|
invoke.mockImplementation(async (command, input) => ({
|
|
config: {
|
|
selectedModelId:
|
|
command === 'select_game_creator_model'
|
|
? (input as { modelId: string }).modelId
|
|
: 'private-old-model',
|
|
},
|
|
}));
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await waitFor(() => expect(onReady).toHaveBeenLastCalledWith(true));
|
|
expect(screen.queryByText('private-old-model')).toBeNull();
|
|
expect(invoke).toHaveBeenCalledWith('select_game_creator_model', {
|
|
modelId: 'quality',
|
|
});
|
|
expect(
|
|
screen.getByRole('button', { name: '对话模型' }).textContent,
|
|
).toContain('高质量');
|
|
});
|
|
|
|
test('failed catalog can be refreshed without enabling submission', async () => {
|
|
vi.mocked(loadClientLlmModels).mockRejectedValueOnce(new Error('offline'));
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByText('模型列表加载失败');
|
|
expect(onReady).toHaveBeenLastCalledWith(false);
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '刷新模型列表' }));
|
|
await waitFor(() => expect(onReady).toHaveBeenLastCalledWith(true));
|
|
});
|
|
|
|
test('a failed save keeps submission unavailable', async () => {
|
|
invoke.mockImplementation(async (command) => {
|
|
if (command === 'select_game_creator_model') throw new Error('disk full');
|
|
return { config: { selectedModelId: 'quality' } };
|
|
});
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByRole('button', { name: '对话模型' });
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
fireEvent.click(screen.getByRole('option', { name: '快速' }));
|
|
await screen.findByText('模型选择保存失败');
|
|
expect(onReady).toHaveBeenLastCalledWith(false);
|
|
});
|
|
|
|
test('closes the menu when clicking outside', async () => {
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByRole('button', { name: '对话模型' });
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
expect(screen.getByRole('option', { name: '快速' })).not.toBeNull();
|
|
|
|
fireEvent.mouseDown(document.body);
|
|
await waitFor(() =>
|
|
expect(screen.queryByRole('option', { name: '快速' })).toBeNull(),
|
|
);
|
|
});
|
|
|
|
test('closes the menu on Escape', async () => {
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByRole('button', { name: '对话模型' });
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
expect(screen.getByRole('option', { name: '快速' })).not.toBeNull();
|
|
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
await waitFor(() =>
|
|
expect(screen.queryByRole('option', { name: '快速' })).toBeNull(),
|
|
);
|
|
});
|
|
|
|
test('marks the default model in the menu', async () => {
|
|
const onReady = vi.fn();
|
|
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
|
await screen.findByRole('button', { name: '对话模型' });
|
|
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
|
|
|
const qualityOption = screen.getByRole('option', { name: /高质量/ });
|
|
expect(within(qualityOption).getByText('默认')).not.toBeNull();
|
|
const fastOption = screen.getByRole('option', { name: '快速' });
|
|
expect(within(fastOption).queryByText('默认')).toBeNull();
|
|
});
|