Integrate delivered Code navigation and Yuxi channel recovery into Canvas trial source
This commit is contained in:
1 parent
2ca6e3199d
commit
3fe3fdb1c0
38 files changed
+1602
-198
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
|
||||
const projects = vi.hoisted(() => ['A', 'B'].map((name) => ({
|
||||
id: `project-${name.toLowerCase()}`,
|
||||
name,
|
||||
createdAt: '2026-10-13T00:00:00.000Z',
|
||||
updatedAt: '2026-10-13T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-10-13T00:00:00.000Z',
|
||||
})));
|
||||
const codingState = vi.hoisted(() => ({
|
||||
projects,
|
||||
activeProjectId: projects[0].id,
|
||||
activeProject: projects[0],
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
setActiveProject: vi.fn(),
|
||||
removeProject: vi.fn(),
|
||||
createProject: vi.fn(),
|
||||
}));
|
||||
const projectConfigState = vi.hoisted(() => ({
|
||||
load: vi.fn<(projectId: string) => Promise<unknown>>(),
|
||||
remove: vi.fn(),
|
||||
}));
|
||||
const providerState = vi.hoisted(() => ({
|
||||
refreshProviderSnapshot: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (_key: string, fallback: string) => fallback }),
|
||||
}));
|
||||
vi.mock('@/lib/api-client', () => ({ invokeIpc: vi.fn() }));
|
||||
vi.mock('@/hooks/use-current-user-profile', () => ({
|
||||
useCurrentUserProfile: () => ({
|
||||
userProfile: { displayName: '测试用户', avatarUrl: null },
|
||||
profileRequired: false,
|
||||
profileSyncError: null,
|
||||
syncProfileNow: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: (selector: (state: unknown) => unknown) => selector({ user: null, logout: vi.fn() }),
|
||||
}));
|
||||
vi.mock('@/stores/settings', () => ({
|
||||
useSettingsStore: (selector: (state: unknown) => unknown) => selector({ sidebarCollapsed: false }),
|
||||
}));
|
||||
vi.mock('@/stores/coding-workspace', () => ({
|
||||
codingWorkspaceStore: { getState: () => codingState },
|
||||
useCodingWorkspaceStore: (selector: (state: typeof codingState) => unknown) => selector(codingState),
|
||||
}));
|
||||
vi.mock('@/stores/project-config', () => ({
|
||||
useProjectConfigStore: (selector: (state: typeof projectConfigState) => unknown) => selector(projectConfigState),
|
||||
}));
|
||||
vi.mock('@/stores/providers', () => ({
|
||||
useProviderStore: (selector: (state: typeof providerState) => unknown) => selector(providerState),
|
||||
}));
|
||||
vi.mock('@/pages/Chat/CodingProjectConversations', () => ({
|
||||
CodingProjectConversations: ({ archived }: { archived: boolean }) => (
|
||||
<div data-testid="conversation-list-mode">{archived ? 'archived' : 'recent'}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock('@/components/account/PointWallet', () => ({ PointWallet: () => null }));
|
||||
vi.mock('@/components/community/CommunityLink', () => ({ CommunityLink: () => null }));
|
||||
vi.mock('@/components/layout/ModuleSwitcher', () => ({ ModuleSwitcher: () => null }));
|
||||
vi.mock('@/components/layout/ImageWorkspaceSidebar', () => ({ ImageWorkspaceSidebar: () => null }));
|
||||
vi.mock('@/components/layout/SidebarUpdateButton', () => ({ SidebarUpdateButton: () => null }));
|
||||
vi.mock('@/components/profile/UserProfileDialog', () => ({ UserProfileDialog: () => null }));
|
||||
|
||||
function LocationProbe() {
|
||||
return <div data-testid="location">{useLocation().pathname}</div>;
|
||||
}
|
||||
|
||||
async function renderSidebar() {
|
||||
render(<MemoryRouter initialEntries={['/chat']}><Sidebar /><LocationProbe /></MemoryRouter>);
|
||||
await act(async () => undefined);
|
||||
projectConfigState.load.mockClear();
|
||||
}
|
||||
|
||||
async function selectProjectAction(projectName: string, action: string) {
|
||||
fireEvent.keyDown(screen.getByRole('button', { name: `项目操作:${projectName}` }), { key: 'Enter' });
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: action }));
|
||||
}
|
||||
|
||||
describe('Sidebar project actions', () => {
|
||||
beforeEach(() => {
|
||||
codingState.activeProjectId = projects[0].id;
|
||||
codingState.activeProject = projects[0];
|
||||
codingState.setActiveProject.mockReset();
|
||||
codingState.setActiveProject.mockImplementation(async (projectId: string) => {
|
||||
const project = projects.find((item) => item.id === projectId)!;
|
||||
codingState.activeProjectId = projectId;
|
||||
codingState.activeProject = project;
|
||||
return project;
|
||||
});
|
||||
projectConfigState.load.mockReset();
|
||||
projectConfigState.load.mockResolvedValue({ status: 'valid', config: {} });
|
||||
});
|
||||
|
||||
it.each(['项目设置', '已归档对话'])('%s for the current project supersedes an earlier pending project open', async (action) => {
|
||||
await renderSidebar();
|
||||
let resolveProjectB!: (value: unknown) => void;
|
||||
const pendingProjectB = new Promise((resolve) => { resolveProjectB = resolve; });
|
||||
projectConfigState.load.mockImplementation((projectId) => projectId === projects[1].id
|
||||
? pendingProjectB
|
||||
: Promise.resolve({ status: 'valid', config: {} }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '进入项目 B' }));
|
||||
expect(projectConfigState.load).toHaveBeenCalledWith(projects[1].id);
|
||||
expect(codingState.setActiveProject).not.toHaveBeenCalled();
|
||||
await selectProjectAction('A', action);
|
||||
await waitFor(() => {
|
||||
if (action === '项目设置') expect(screen.getByTestId('location')).toHaveTextContent('/project-config');
|
||||
else expect(screen.getByTestId('conversation-list-mode')).toHaveTextContent('archived');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolveProjectB({ status: 'valid', config: {} });
|
||||
await pendingProjectB;
|
||||
});
|
||||
|
||||
expect(codingState.activeProjectId).toBe(projects[0].id);
|
||||
expect(codingState.setActiveProject).not.toHaveBeenCalledWith(projects[1].id);
|
||||
if (action === '项目设置') expect(screen.getByTestId('location')).toHaveTextContent('/project-config');
|
||||
else {
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('/chat');
|
||||
expect(screen.getByTestId('conversation-list-mode')).toHaveTextContent('archived');
|
||||
}
|
||||
});
|
||||
|
||||
it.each(['项目设置', '已归档对话'])('rejects %s for an inactive project with invalid configuration', async (action) => {
|
||||
await renderSidebar();
|
||||
projectConfigState.load.mockResolvedValue({ status: 'invalid', config: null, error: '配置文件无效' });
|
||||
|
||||
await selectProjectAction('B', action);
|
||||
|
||||
expect(await screen.findByRole('alertdialog')).toHaveTextContent('配置文件无效');
|
||||
expect(projectConfigState.load).toHaveBeenCalledWith(projects[1].id);
|
||||
expect(codingState.setActiveProject).not.toHaveBeenCalled();
|
||||
expect(codingState.activeProjectId).toBe(projects[0].id);
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('/chat');
|
||||
expect(screen.getByTestId('conversation-list-mode')).toHaveTextContent('recent');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user