Makelore 2.0 initial clean snapshot
This commit is contained in:
428
tests/unit/sidebar-opencode-projects.test.tsx
Normal file
428
tests/unit/sidebar-opencode-projects.test.tsx
Normal file
@@ -0,0 +1,428 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
import { useOpencodeStore } from '@/stores/opencode';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useUpdateStore } from '@/stores/update';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import { useUserProfileStore } from '@/stores/user-profile';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
import { createProjectConfig } from '../../shared/project-config';
|
||||
|
||||
const hostApiFetchMock = vi.fn();
|
||||
const invokeIpcMock = vi.fn();
|
||||
const navigateMock = vi.fn();
|
||||
vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), ensureHostApiToken: vi.fn().mockResolvedValue('test-token') }));
|
||||
vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args) }));
|
||||
vi.mock('react-router-dom', async (importOriginal) => ({ ...(await importOriginal<typeof import('react-router-dom')>()), useNavigate: () => navigateMock }));
|
||||
|
||||
describe('Sidebar project initialization flow', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.electron.imageWorkspaceLocalDevelopment = false;
|
||||
useSettingsStore.setState({ sidebarCollapsed: false });
|
||||
useAuthStore.setState({ initialized: true, loading: false, error: null, accessToken: null, refreshToken: null, tokenType: null, expiresAt: null, user: null });
|
||||
useUserProfileStore.setState({ profilesByUserId: {} });
|
||||
useImageWorkspaceStore.getState().reset();
|
||||
useUpdateStore.setState({
|
||||
status: 'idle',
|
||||
currentVersion: '0.9.1',
|
||||
updateInfo: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
isInitialized: true,
|
||||
isInitializing: false,
|
||||
autoInstallCountdown: null,
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
downloadUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
installUpdate: vi.fn(),
|
||||
});
|
||||
useProjectConfigStore.setState({ configsByProjectId: {}, knowledgeByProjectId: {}, errorsByProjectId: {}, loadingProjectId: null });
|
||||
useOpencodeStore.setState({ projects: [], activeProject: null, status: { state: 'stopped', port: 4096 }, sessions: [], sessionsByProjectId: {}, selectedSessionId: null, loading: false, error: null });
|
||||
invokeIpcMock.mockResolvedValue({ canceled: true, filePaths: [] });
|
||||
});
|
||||
|
||||
it('requires a personal profile name after login and saves optional details', async () => {
|
||||
useAuthStore.setState({
|
||||
user: { username: 'login@example.com', userId: 'user-1', tenantId: null, deptId: null, authorities: [] },
|
||||
accessToken: 'token',
|
||||
});
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
|
||||
if (path === '/api/works/billing/token-usage') return {};
|
||||
if (path === '/api/works/user/agent-profile') {
|
||||
if (init?.method === 'PUT') {
|
||||
return {
|
||||
success: true,
|
||||
profile: {
|
||||
display_name: '小明',
|
||||
age: 16,
|
||||
gender: 'undisclosed',
|
||||
share_age_with_agents: true,
|
||||
share_gender_with_agents: false,
|
||||
analysis_enabled: true,
|
||||
completed: true,
|
||||
version: 1,
|
||||
updated_at: '2026-07-12T00:00:00Z',
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
profile: {
|
||||
display_name: null,
|
||||
age: null,
|
||||
gender: null,
|
||||
share_age_with_agents: false,
|
||||
share_gender_with_agents: false,
|
||||
analysis_enabled: true,
|
||||
completed: false,
|
||||
version: 0,
|
||||
updated_at: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
expect(await screen.findByText('首次使用前请先填写名字。个人资料与登录账号信息相互独立。')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '关闭个人资料' })).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText(/名字/), { target: { value: '小明' } });
|
||||
fireEvent.change(screen.getByLabelText('年龄(选填)'), { target: { value: '16' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存个人资料' }));
|
||||
await waitFor(() => expect(useUserProfileStore.getState().profilesByUserId['user-1']).toMatchObject({ displayName: '小明', age: 16, version: 1 }));
|
||||
});
|
||||
|
||||
it('shows create project first and removes redundant project navigation actions', async () => {
|
||||
hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
expect(screen.getByRole('img', { name: 'Makelore logo' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Makelore 工作台')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/NITU/)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-create-project')).toHaveTextContent('新建项目');
|
||||
expect(screen.queryByTestId('sidebar-nav-asset-square')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sidebar-nav-project-config')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sidebar-nav-agent-chat')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '添加已有项目' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('课程路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('伙伴广场')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-module-switcher')).toHaveTextContent('切换模块');
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByTestId('sidebar-module-programming')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
|
||||
expect(screen.getByTestId('sidebar-module-programming')).toHaveTextContent('Makelore Code');
|
||||
expect(screen.getByTestId('sidebar-module-painting')).toHaveTextContent('Makelore Canvas');
|
||||
expect(screen.queryByTestId('sidebar-nav-image-canvas')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText(/暂无真实项目/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('places the module switcher above personal profile in the lower-left area', async () => {
|
||||
hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-member-menu-trigger'));
|
||||
const switcher = screen.getByTestId('sidebar-module-switcher');
|
||||
const profileMenuItem = screen.getByRole('menuitem', { name: '个人资料' });
|
||||
expect(Boolean(switcher.compareDocumentPosition(profileMenuItem) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
||||
});
|
||||
|
||||
it('shows an available update beside the lower-left account control', async () => {
|
||||
hostApiFetchMock.mockResolvedValue({ projects: [], activeProject: null });
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
|
||||
const account = screen.getByTestId('sidebar-member-menu-trigger');
|
||||
expect(screen.queryByTestId('sidebar-update-button')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
useUpdateStore.setState({
|
||||
status: 'available',
|
||||
updateInfo: { version: '0.9.2' },
|
||||
});
|
||||
});
|
||||
|
||||
const update = await screen.findByTestId('sidebar-update-button');
|
||||
expect(account.parentElement).toBe(update.parentElement);
|
||||
expect(Boolean(account.compareDocumentPosition(update) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
||||
expect(update).toHaveAccessibleName('下载新版本 0.9.2');
|
||||
});
|
||||
|
||||
it('creates a path-backed game project with the selected replacement template and opens configuration', async () => {
|
||||
const project = { id: 'prj_game', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '2026-07-11T00:00:00.000Z', updatedAt: '2026-07-11T00:00:00.000Z', lastOpenedAt: '2026-07-11T00:00:00.000Z' };
|
||||
const config = createProjectConfig('game-development');
|
||||
invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo/星星收集器'] });
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
|
||||
if (path === '/api/opencode/projects/create') return { success: true, project, projects: [project], activeProject: null, config };
|
||||
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/projects/active' && init?.method === 'POST') return { success: true, project, projects: [project], activeProject: project };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
|
||||
expect(screen.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
|
||||
expect(screen.queryByLabelText('项目名称')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
|
||||
await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo/星星收集器'));
|
||||
expect(screen.getByText('将直接接入此文件夹,项目名称使用“星星收集器”,不会重命名目录。')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('project-template-option-youth-ai-course')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('project-template-option-standard-development')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('project-template-option-game-development'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认创建' }));
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器', templateId: 'game-development' }) }));
|
||||
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/project-config'));
|
||||
});
|
||||
|
||||
it('keeps creating a named child folder when that directory mode is selected', async () => {
|
||||
const project = { id: 'prj_child', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '', updatedAt: '', lastOpenedAt: '' };
|
||||
const config = createProjectConfig('standard-development');
|
||||
invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo'] });
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
|
||||
if (path === '/api/opencode/projects/create') return { success: true, project, projects: [project], activeProject: null, config };
|
||||
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/projects/active' && init?.method === 'POST') return { success: true, project, projects: [project], activeProject: project };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
|
||||
fireEvent.click(screen.getByRole('radio', { name: '在所选位置新建下级文件夹' }));
|
||||
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '星星收集器' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
|
||||
await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo'));
|
||||
fireEvent.click(screen.getByTestId('project-template-option-standard-development'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认创建' }));
|
||||
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parentPath: 'D:/repo',
|
||||
projectName: '星星收集器',
|
||||
templateId: 'standard-development',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('opens initialized projects directly in Agent collaboration', async () => {
|
||||
const project = { id: 'prj_ready', path: 'D:/repo/ready', name: 'ready', createdAt: '', updatedAt: '', lastOpenedAt: '' };
|
||||
const config = { ...createProjectConfig('standard-development'), initialized: true };
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
|
||||
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/projects/active') return { success: true, project, projects: [project], activeProject: project };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
fireEvent.click(await screen.findByRole('button', { name: `进入项目 ${project.name}` }));
|
||||
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/opencode-chat'));
|
||||
});
|
||||
|
||||
it('removes a project card when its local project configuration is missing', async () => {
|
||||
const project = { id: 'prj_missing', path: 'D:/repo/missing', name: 'missing', createdAt: '', updatedAt: '', lastOpenedAt: '' };
|
||||
const staleConfig = createProjectConfig('standard-development');
|
||||
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: staleConfig }, knowledgeByProjectId: {}, errorsByProjectId: {}, loadingProjectId: null });
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
|
||||
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'missing', knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/projects/remove' && init?.method === 'POST') return { success: true, removedProjectId: project.id, projects: [], activeProject: null };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
fireEvent.click(await screen.findByRole('button', { name: `配置项目 ${project.name}` }));
|
||||
expect(await screen.findByTestId('project-template-error')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除项目卡片' }));
|
||||
expect(screen.getByRole('dialog', { name: `移除项目“${project.name}”?` })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认移除' }));
|
||||
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/remove', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ projectId: project.id }),
|
||||
}));
|
||||
await waitFor(() => expect(screen.queryByTestId(`sidebar-course-project-${project.id}`)).not.toBeInTheDocument());
|
||||
expect(useProjectConfigStore.getState().configsByProjectId[project.id]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shows project identity with explicit enter and configuration actions', async () => {
|
||||
const project = { id: 'prj_config', path: 'D:/repo/config', name: 'config', createdAt: '', updatedAt: '', lastOpenedAt: '' };
|
||||
const config = {
|
||||
...createProjectConfig('standard-development'),
|
||||
initialized: true,
|
||||
agents: createProjectConfig('standard-development').agents.map((agent, index) => ({
|
||||
...agent,
|
||||
name: ['小策', '小设', '小码', '小发'][index],
|
||||
avatarId: ['avatar-16', 'avatar-15', 'avatar-14', 'avatar-13'][index],
|
||||
})),
|
||||
};
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [project], activeProject: null };
|
||||
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/projects/active') return { success: true, project, projects: [project], activeProject: project };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
render(<MemoryRouter><Sidebar /></MemoryRouter>);
|
||||
const card = await screen.findByTestId(`sidebar-course-project-${project.id}`);
|
||||
await waitFor(() => expect(card).toHaveTextContent('普通开发项目'));
|
||||
expect(card).toHaveTextContent(project.name);
|
||||
expect(card).not.toHaveTextContent(project.path);
|
||||
expect(card).not.toHaveTextContent('已初始化');
|
||||
expect(card).not.toHaveTextContent('最后活跃');
|
||||
expect(card).not.toHaveTextContent('移除项目');
|
||||
expect(screen.getByLabelText('项目 Agent').children).toHaveLength(4);
|
||||
expect(screen.getByTitle('小策').querySelector('img')).toHaveAttribute('src', getAgentAvatarSrc('avatar-16'));
|
||||
expect(screen.getByTitle('小设').querySelector('img')).toHaveAttribute('src', getAgentAvatarSrc('avatar-15'));
|
||||
expect(screen.getByRole('button', { name: `进入项目 ${project.name}` })).toHaveTextContent('进入项目');
|
||||
fireEvent.click(screen.getByRole('button', { name: `配置项目 ${project.name}` }));
|
||||
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/project-config'));
|
||||
});
|
||||
|
||||
it('switches the sidebar to cloud projects and Agents inside the AI painting module', async () => {
|
||||
useAuthStore.setState({
|
||||
accessToken: 'cloud-token',
|
||||
expiresAt: Date.now() + 60_000,
|
||||
});
|
||||
const workspace = {
|
||||
activeProjectId: 'cloud-project',
|
||||
capabilities: {
|
||||
modes: [],
|
||||
models: [],
|
||||
aspectRatios: [],
|
||||
resolutions: [],
|
||||
outputCounts: [],
|
||||
maxReferenceImages: 0,
|
||||
referenceUpload: { enabled: false, acceptedMimeTypes: [] },
|
||||
},
|
||||
projects: [{
|
||||
id: 'cloud-project',
|
||||
name: '云端概念设计',
|
||||
activeAgentId: 'cloud-agent',
|
||||
agents: [{ id: 'cloud-agent', name: '概念设计 Agent' }],
|
||||
messages: [],
|
||||
}],
|
||||
};
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/works/image-workspace') return { success: true, workspace };
|
||||
if (path === '/api/works/image-workspace/projects' && init?.method === 'POST') {
|
||||
return { success: true, workspace };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/projects/cloud-project/agents' && init?.method === 'POST') {
|
||||
return {
|
||||
success: true,
|
||||
workspace: {
|
||||
...workspace,
|
||||
projects: [{
|
||||
...workspace.projects[0],
|
||||
agents: [
|
||||
...workspace.projects[0].agents,
|
||||
{ id: 'cloud-agent-two', name: '新 Agent' },
|
||||
],
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
|
||||
render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);
|
||||
|
||||
expect(await screen.findByTestId('sidebar-image-project-cloud-project')).toHaveTextContent('云端概念设计');
|
||||
expect(screen.getByText('概念设计 Agent')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-create-image-project')).toBeEnabled();
|
||||
expect(screen.queryByTestId('sidebar-create-project')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('项目路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
|
||||
expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects')).toBe(false);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '为 云端概念设计 添加 Agent' }));
|
||||
expect(await screen.findByText('新 Agent')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
|
||||
expect(screen.getByRole('dialog', { name: '新建项目' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('项目名称')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('项目路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '角色设定' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建项目' }));
|
||||
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
|
||||
'/api/works/image-workspace/projects',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-NianCode-Access-Token': 'cloud-token' },
|
||||
body: JSON.stringify({ name: '角色设定' }),
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
it('keeps production project copy while the development adapter bypasses cloud login', async () => {
|
||||
window.electron.imageWorkspaceLocalDevelopment = true;
|
||||
const adapterProject = {
|
||||
id: 'local-project',
|
||||
name: '概念设计',
|
||||
activeAgentId: 'local-agent',
|
||||
agents: [{ id: 'local-agent', name: '视觉创作 Agent' }],
|
||||
messages: [],
|
||||
};
|
||||
let workspace = {
|
||||
activeProjectId: adapterProject.id as string | null,
|
||||
capabilities: {
|
||||
modes: [],
|
||||
models: [],
|
||||
aspectRatios: [],
|
||||
resolutions: [],
|
||||
outputCounts: [],
|
||||
maxReferenceImages: 0,
|
||||
referenceUpload: { enabled: false, acceptedMimeTypes: [] },
|
||||
},
|
||||
projects: [adapterProject],
|
||||
};
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/works/image-workspace' && !init?.method) {
|
||||
return { success: true, workspace };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/projects' && init?.method === 'POST') {
|
||||
const createdProject = {
|
||||
id: 'local-project-two',
|
||||
name: '角色设计',
|
||||
activeAgentId: 'local-agent-two',
|
||||
agents: [{ id: 'local-agent-two', name: '视觉创作 Agent' }],
|
||||
messages: [],
|
||||
};
|
||||
workspace = {
|
||||
...workspace,
|
||||
activeProjectId: createdProject.id,
|
||||
projects: [...workspace.projects, createdProject],
|
||||
};
|
||||
return { success: true, workspace };
|
||||
}
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
|
||||
const { container } = render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);
|
||||
|
||||
expect(await screen.findByTestId('sidebar-image-project-local-project')).toHaveTextContent('概念设计');
|
||||
expect(screen.getByTestId('sidebar-create-image-project')).toHaveTextContent('新建项目');
|
||||
expect(screen.getByTestId('sidebar-image-projects')).toBeInTheDocument();
|
||||
expect(container).not.toHaveTextContent('本地开发');
|
||||
expect(container).not.toHaveTextContent('本地项目');
|
||||
expect(screen.queryByTestId('sidebar-reset-local-image-workspace')).not.toBeInTheDocument();
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-workspace', {});
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
|
||||
expect(screen.getByRole('dialog', { name: '新建项目' })).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '角色设计' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建项目' }));
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
|
||||
'/api/works/image-workspace/projects',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: '角色设计' }),
|
||||
},
|
||||
));
|
||||
expect(await screen.findByText('角色设计')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user