merge: integrate coding default page

# Conflicts:
#	src/components/layout/Sidebar.tsx
#	tests/unit/coding-chat-panel.test.tsx
This commit is contained in:
inman
2026-09-07 10:10:34 +08:00
12 changed files with 487 additions and 36 deletions

View File

@@ -60,6 +60,12 @@ const project: CodingProjectSummary = {
updatedAt: '2026-08-23T00:00:00.000Z',
lastOpenedAt: '2026-08-23T00:00:00.000Z',
};
const secondProject: CodingProjectSummary = {
...project,
id: 'project-2',
name: 'Second local project',
lastOpenedAt: '2026-08-22T00:00:00.000Z',
};
const agent: CodingProjectAgent = {
id: 'agent-1',
avatarId: 'default',
@@ -217,6 +223,40 @@ describe('CodingChatPanel first Conversation', () => {
expect(openProjectSettings).toHaveBeenCalledOnce();
});
it('shows a branded create CTA and horizontal existing-project cards before a project is selected', async () => {
projectApi.list.mockResolvedValue({ projects: [project, secondProject], activeProjectId: null });
const onCreateProject = vi.fn();
const onOpenProject = vi.fn();
const onOpenProjectSettings = vi.fn();
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
render(
<CodingChatPanel
onCreateProject={onCreateProject}
onOpenProject={onOpenProject}
onOpenProjectSettings={onOpenProjectSettings}
/>,
);
expect(await screen.findByTestId('coding-chat-empty-project')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: '麦洛 M 标识' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '新增项目' })).toBeInTheDocument();
expect(screen.getByTestId('coding-existing-projects')).toHaveTextContent('Local project');
expect(screen.getByTestId('coding-existing-projects')).toHaveTextContent('Second local project');
expect(screen.queryByText('已有项目')).not.toBeInTheDocument();
expect(screen.queryByText('选择一个项目继续')).not.toBeInTheDocument();
expect(screen.queryByTestId('coding-project-starter')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '项目与插件设置' })).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('coding-start-project-button'));
expect(onCreateProject).toHaveBeenCalledOnce();
fireEvent.click(screen.getByRole('button', { name: '进入项目 Second local project' }));
expect(onOpenProject).toHaveBeenCalledWith(secondProject.id);
expect(onOpenProjectSettings).not.toHaveBeenCalled();
});
it('makes the first-Conversation textarea editable while runtime metadata is held', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
@@ -233,6 +273,8 @@ describe('CodingChatPanel first Conversation', () => {
const view = render(<CodingChatPanel />);
const textbox = await screen.findByRole('textbox');
expect(screen.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: '麦洛 M 标识' })).toBeInTheDocument();
expect(textbox).toBeEnabled();
expect(projectApi.create).toHaveBeenCalledOnce();
expect(conversationApi.snapshot).toHaveBeenCalledWith(conversation.id);
@@ -940,7 +982,7 @@ describe('CodingChatPanel first Conversation', () => {
target: { files },
});
expect(await screen.findByText('每条消息最多添加 16 张图片。')).toBeInTheDocument();
expect(screen.getAllByRole('img')).toHaveLength(16);
expect(screen.getAllByRole('img', { name: /^image-\d+\.png$/u })).toHaveLength(16);
expect(codingConversationStore.getState().entriesByConversationId[conversation.id])
.toMatchObject({ loadState: 'live', error: null });
await waitFor(() => expect(screen.getByRole('button', { name: '发送' })).toBeEnabled());

View File

@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import {
requestCodingProjectCreation,
requestCodingProjectOpen,
subscribeCodingProjectCreationRequest,
subscribeCodingProjectOpenRequest,
} from '@/lib/coding-project-entry';
describe('coding project entry events', () => {
it('delivers project creation requests and removes the listener on unsubscribe', () => {
const listener = vi.fn();
const unsubscribe = subscribeCodingProjectCreationRequest(listener);
requestCodingProjectCreation();
expect(listener).toHaveBeenCalledOnce();
unsubscribe();
requestCodingProjectCreation();
expect(listener).toHaveBeenCalledOnce();
});
it('delivers the selected project id and ignores requests after unsubscribe', () => {
const listener = vi.fn();
const unsubscribe = subscribeCodingProjectOpenRequest(listener);
requestCodingProjectOpen('project-2');
expect(listener).toHaveBeenCalledWith('project-2');
unsubscribe();
requestCodingProjectOpen('project-3');
expect(listener).toHaveBeenCalledOnce();
});
});