收口 Makelore 客户端变更
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-12 22:35:06 +08:00
parent 253bad8b40
commit f4113a872f
232 changed files with 4617 additions and 20121 deletions

View File

@@ -1,8 +1,9 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { StrictMode, type ComponentProps } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentConversationSidebar } from '@/components/opencode/AgentConversationSidebar';
import type { ConfiguredModelOption } from '@/lib/model-options';
import { useSettingsStore } from '@/stores/settings';
import type { ProjectAgentConfig } from '../../shared/project-config';
import type { ProjectSessionMetadata } from '../../shared/project-conversations';
import type { OpencodeSession } from '@/types/opencode';
@@ -49,7 +50,10 @@ function metadata(sessionId: string, agentId: string, overrides: Partial<Project
};
}
function renderSidebar(overrides: Partial<ComponentProps<typeof AgentConversationSidebar>> = {}) {
function renderSidebar(
overrides: Partial<ComponentProps<typeof AgentConversationSidebar>> = {},
options: { strictMode?: boolean } = {},
) {
const callbacks = {
onSelectAgent: vi.fn(),
onSelectSession: vi.fn(),
@@ -62,10 +66,9 @@ function renderSidebar(overrides: Partial<ComponentProps<typeof AgentConversatio
onRestoreSession: vi.fn().mockResolvedValue(undefined),
onDeleteSession: vi.fn().mockResolvedValue(undefined),
onTogglePinAgent: vi.fn().mockResolvedValue(undefined),
onOpenDevicePreview: vi.fn(),
onOpenProjectSettings: vi.fn(),
};
render(
const sidebar = (
<AgentConversationSidebar
agents={[agent('agent-a', '小明'), agent('agent-b', '小红', { pinned: true })]}
sessions={[session('ses-a', '第一次聊天')]}
@@ -80,19 +83,90 @@ function renderSidebar(overrides: Partial<ComponentProps<typeof AgentConversatio
modelOptions={[modelOption]}
{...callbacks}
{...overrides}
/>,
/>
);
render(options.strictMode ? <StrictMode>{sidebar}</StrictMode> : sidebar);
return callbacks;
}
describe('AgentConversationSidebar', () => {
beforeEach(() => {
useSettingsStore.setState({ sidebarCollapsed: false });
});
afterEach(() => {
cleanup();
useSettingsStore.setState({ sidebarCollapsed: false });
});
it('uses a flush square sidebar surface without an outer card frame', () => {
renderSidebar();
const sidebar = screen.getByTestId('agent-conversation-sidebar');
expect(sidebar).toHaveClass('rounded-none', 'border-r');
expect(sidebar).toHaveClass('no-drag', 'pointer-events-auto', 'absolute', 'inset-y-0', 'left-0', 'w-[256px]', 'min-w-[256px]', 'max-w-[256px]', 'z-[60]', 'rounded-none', 'border-r');
expect(sidebar).not.toHaveClass('rounded-xl', 'shadow-soft');
expect(screen.getByTestId('agent-conversation-sidebar-content')).toHaveClass('p-0');
expect(screen.getByTestId('agent-conversation-project-context')).toHaveTextContent('Makelore 工作台');
});
it('keeps the conversation actions in the conversation list title rail', () => {
renderSidebar();
const sidebar = screen.getByTestId('agent-conversation-sidebar');
const header = screen.getByTestId('agent-conversation-sidebar-header');
expect(sidebar).not.toContainElement(header);
const actionPortal = screen.getByTestId('agent-conversation-sidebar-titlebar-portal');
expect(actionPortal).toContainElement(header);
expect(actionPortal).toHaveClass('border-r');
expect(actionPortal).toHaveStyle({ left: '256px' });
expect(header).toHaveClass('w-full', 'shrink-0', 'items-center');
expect(header).not.toHaveClass('py-1');
expect(header).toHaveClass('pointer-events-none');
expect(header).not.toHaveClass('drag-region');
expect(header.firstElementChild).toHaveClass('no-drag', 'pointer-events-auto');
expect(screen.getByTestId('agent-conversation-dialog-context-portal')).toHaveStyle({ left: '512px' });
expect(screen.getByTestId('agent-conversation-dialog-context')).toContainElement(screen.getByTestId('agent-conversation-project-context'));
expect(screen.queryByRole('button', { name: '真机预览' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '项目设置' })).toHaveClass('no-drag');
const createButton = screen.getByRole('button', { name: '创建伙伴' });
expect(createButton).toHaveClass('no-drag', 'text-brand');
expect(createButton.querySelector('svg')).toHaveClass('text-brand');
expect(createButton).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(createButton);
expect(createButton).toHaveClass('bg-brand', 'text-primary-foreground');
expect(createButton.querySelector('svg')).toHaveClass('text-primary-foreground');
expect(createButton).toHaveAttribute('aria-expanded', 'true');
});
it('keeps both title rails aligned when the main sidebar is collapsed', () => {
useSettingsStore.setState({ sidebarCollapsed: true });
renderSidebar();
const sidebar = screen.getByTestId('agent-conversation-sidebar');
const header = screen.getByTestId('agent-conversation-sidebar-header');
const actionPortal = screen.getByTestId('agent-conversation-sidebar-titlebar-portal');
const contextPortal = screen.getByTestId('agent-conversation-dialog-context-portal');
expect(sidebar).not.toContainElement(header);
expect(actionPortal).toContainElement(header);
expect(actionPortal).toHaveClass('pointer-events-none');
expect(actionPortal).toHaveStyle({ left: '0px' });
expect(contextPortal).toHaveStyle({ left: '256px' });
});
it('keeps the agent creation dialog above the conversation sidebar', () => {
renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
expect(screen.getByRole('dialog')).toHaveClass('z-[110]');
});
it('keeps the fixed actions mounted through React StrictMode effect replay', () => {
renderSidebar({}, { strictMode: true });
expect(screen.getByRole('button', { name: '项目设置' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '创建伙伴' })).toBeInTheDocument();
});
it('opens project settings from the conversation header', () => {
@@ -102,21 +176,13 @@ describe('AgentConversationSidebar', () => {
expect(callbacks.onOpenProjectSettings).toHaveBeenCalledOnce();
});
it('opens the project device preview from the conversation header', () => {
const callbacks = renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '真机预览' }));
expect(callbacks.onOpenDevicePreview).toHaveBeenCalledOnce();
});
it('shows only the centered project-partner heading in the conversation header', () => {
it('keeps the conversation actions at the top without a redundant workspace heading', () => {
renderSidebar();
expect(screen.queryByText('项目联系人')).not.toBeInTheDocument();
const heading = screen.getByRole('heading', { name: '我的项目空间' });
expect(heading).toHaveClass('text-left');
expect(heading.parentElement).toHaveClass('relative', 'px-3', 'pr-28');
expect(heading.parentElement).not.toHaveClass('justify-center');
expect(screen.queryByText('项目伙伴')).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: '我的项目空间' })).not.toBeInTheDocument();
expect(screen.getByTestId('agent-conversation-sidebar-header')).toHaveClass('justify-end', 'bg-surface-tertiary');
expect(screen.getByTestId('agent-conversation-sidebar-header')).not.toHaveClass('border-b');
});
it('uses square corners for each project contact card', () => {
@@ -133,9 +199,8 @@ describe('AgentConversationSidebar', () => {
const contacts = screen.getAllByTestId(/^project-agent-chat-/);
expect(contacts[0]).toHaveAttribute('data-testid', 'project-agent-chat-agent-b');
expect(screen.getByText('小明')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('有新消息')).toBeInTheDocument();
expect(screen.getByTestId('agent-status-row-agent-a')).toContainElement(screen.getByText('2'));
expect(within(screen.getByTestId('agent-status-row-agent-a')).getByText('2')).toBeInTheDocument();
expect(screen.queryByText('等待开始')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
@@ -146,11 +211,15 @@ describe('AgentConversationSidebar', () => {
const callbacks = renderSidebar({ selectedAgentId: 'agent-a' });
expect(screen.getByTestId('project-agent-chat-agent-a')).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByTestId('project-agent-chat-agent-a').parentElement?.parentElement).toHaveAttribute('data-selected', 'true');
expect(screen.getByTestId('project-agent-chat-agent-b').parentElement?.parentElement).toHaveAttribute('data-selected', 'false');
expect(screen.getByLabelText('小明的会话列表')).toBeInTheDocument();
expect(screen.getByLabelText('小明的会话列表').parentElement?.parentElement).toHaveAttribute('data-state', 'open');
expect(screen.getByLabelText('小红的会话列表').parentElement?.parentElement).toHaveAttribute('data-state', 'closed');
expect(screen.getByText('第一次聊天')).toBeInTheDocument();
expect(screen.queryByText('小明的对话')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '返回联系人列表' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '联系人设置' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '返回伙伴列表' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '伙伴设置' })).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
expect(callbacks.onSelectAgent).toHaveBeenCalledWith('agent-a');
@@ -183,7 +252,7 @@ describe('AgentConversationSidebar', () => {
it('centers the pin and archive actions vertically on the contact card', () => {
renderSidebar();
const pinButton = screen.getByRole('button', { name: '置顶联系人' });
const pinButton = screen.getByRole('button', { name: '置顶伙伴' });
const actionRail = pinButton.parentElement;
expect(actionRail).toHaveClass('top-1/2', '-translate-y-1/2', 'flex-col');
});
@@ -204,12 +273,14 @@ describe('AgentConversationSidebar', () => {
expect(screen.getByTitle(longMessage)).toHaveTextContent(/$/u);
expect(screen.queryByText('其他会话')).not.toBeInTheDocument();
expect(screen.getByText('更多会话1')).toBeInTheDocument();
const moreSessions = screen.getByText('更多会话1').closest('details');
expect(moreSessions).not.toHaveAttribute('open');
const moreSessions = screen.getByRole('button', { name: '更多会话1' });
expect(moreSessions).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(screen.getByText('更多会话1'));
expect(moreSessions).toHaveAttribute('open');
fireEvent.click(moreSessions);
expect(moreSessions).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(moreSessions);
expect(moreSessions).toHaveAttribute('aria-expanded', 'false');
});
it('removes the session bubble icon and keeps archive hidden until the card is hovered', () => {
@@ -224,8 +295,8 @@ describe('AgentConversationSidebar', () => {
it('creates a contact from the required basics and confirms archive actions', async () => {
const callbacks = renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '创建联系人' }));
fireEvent.change(screen.getByLabelText(/联系人名称/), { target: { value: '小李' } });
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
fireEvent.change(screen.getByLabelText(/伙伴名称/), { target: { value: '小李' } });
fireEvent.change(screen.getByLabelText(/职责简述/), { target: { value: '负责整理资料。' } });
fireEvent.click(screen.getByRole('button', { name: '更换头像' }));
fireEvent.click(screen.getByRole('button', { name: '选择头像:像素伙伴 3' }));
@@ -239,15 +310,15 @@ describe('AgentConversationSidebar', () => {
skillIds: [],
}));
fireEvent.click(screen.getByLabelText('归档联系人 小明'));
expect(screen.getByRole('dialog', { name: '归档联系人“小明”?' })).toBeInTheDocument();
fireEvent.click(screen.getByLabelText('归档伙伴 小明'));
expect(screen.getByRole('dialog', { name: '归档伙伴“小明”?' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '取消' }));
expect(callbacks.onArchiveAgent).not.toHaveBeenCalled();
});
it('shows advanced creation settings directly below the basics', () => {
renderSidebar();
fireEvent.click(screen.getByRole('button', { name: '创建联系人' }));
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
expect(screen.getByText('高级设置(提示词与技能)')).toBeInTheDocument();
expect(screen.getByLabelText('Agent 提示词')).toBeInTheDocument();
@@ -264,10 +335,11 @@ describe('AgentConversationSidebar', () => {
],
selectedAgentId: 'agent-a',
});
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
fireEvent.click(screen.getByRole('button', { name: '已归档会话1' }));
expect(screen.getByText('旧伙伴')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '恢复联系人 旧伙伴' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '永久删除联系人 旧伙伴' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '恢复伙伴 旧伙伴' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '永久删除伙伴 旧伙伴' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '恢复会话 新对话' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '永久删除会话 新对话' })).toBeInTheDocument();
});
@@ -293,9 +365,9 @@ describe('AgentConversationSidebar', () => {
conversationSessions: [metadata('ses-old', 'agent-archived', { archivedAt: '2026-07-29T00:00:00.000Z' })],
});
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
fireEvent.click(screen.getByRole('button', { name: '永久删除联系人 旧伙伴' }));
expect(screen.getByRole('dialog', { name: '永久删除联系人“旧伙伴”?' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
fireEvent.click(screen.getByRole('button', { name: '永久删除伙伴 旧伙伴' }));
expect(screen.getByRole('dialog', { name: '永久删除伙伴“旧伙伴”?' })).toBeInTheDocument();
expect(callbacks.onDeleteAgent).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
await waitFor(() => expect(callbacks.onDeleteAgent).toHaveBeenCalledWith(archivedAgent));
@@ -306,7 +378,7 @@ describe('AgentConversationSidebar', () => {
agents: [agent('built-in-archived', '内置伙伴', { builtIn: true, archivedAt: '2026-07-29T00:00:00.000Z' })],
});
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
expect(screen.queryByRole('button', { name: '永久删除联系人 内置伙伴' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /已归档伙伴/ }));
expect(screen.queryByRole('button', { name: '永久删除伙伴 内置伙伴' })).not.toBeInTheDocument();
});
});