Files
makelore/tests/unit/image-workspace-sidebar.test.tsx
T

216 lines
9.6 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ImageWorkspaceSidebar } from '@/components/layout/ImageWorkspaceSidebar';
import { IMAGE_WORKSPACE_CREATE_PROJECT_EVENT } from '@/lib/image-workspace';
import { useAuthStore } from '@/stores/auth';
import { useImageWorkspaceStore } from '@/stores/image-workspace';
import {
designBootstrapFixture,
designSummaryFixture,
designWorkspaceFixture,
} from '../fixtures/design-workspace-v2';
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
function setViewport(desktop: boolean) {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
matches: desktop,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
}
function prepareProjects() {
const workspace = designWorkspaceFixture();
const target = designSummaryFixture({
workspaceId: 'workspace-other',
clientWorkspaceId: 'client-workspace-other',
directionId: 'direction-other',
sessionId: 'session-other',
title: '月球熊猫海报',
updatedAt: '2026-09-01T10:00:00.000Z',
});
const actions = {
load: vi.fn().mockResolvedValue(undefined),
createProject: vi.fn().mockResolvedValue(workspace),
renameProject: vi.fn().mockResolvedValue(workspace),
deleteProject: vi.fn().mockResolvedValue({ workspaceId: target.workspaceId, deleted: true }),
selectProject: vi.fn().mockResolvedValue(undefined),
};
useAuthStore.setState({ isAuthenticated: () => true });
useImageWorkspaceStore.setState({
status: 'ready',
bootstrap: {
...designBootstrapFixture(workspace.workspace),
workspaces: [workspace.workspace, target],
},
activeWorkspaceId: workspace.workspace.workspaceId,
workspace,
deletingWorkspaceId: null,
error: null,
...actions,
});
return { workspace, target, actions };
}
function LocationProbe() {
const location = useLocation();
return <output aria-label="当前页面">{location.pathname}</output>;
}
function renderSidebar({ collapsed = false, hidden = false } = {}) {
return render(
<MemoryRouter initialEntries={['/module-select']}>
<aside hidden={hidden}>
<ImageWorkspaceSidebar sidebarCollapsed={collapsed} />
</aside>
<LocationProbe />
</MemoryRouter>,
);
}
async function openProjectMenu(title: string) {
const trigger = screen.getByRole('button', { name: `项目操作:${title}` });
fireEvent.keyDown(trigger, { key: 'Enter' });
return screen.findByRole('menu');
}
describe('ImageWorkspaceSidebar', () => {
beforeEach(() => {
setViewport(true);
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
it('opens one creation dialog from the collapsed sidebar and submits the trimmed title', async () => {
const { actions } = prepareProjects();
renderSidebar({ collapsed: true });
fireEvent.click(screen.getByRole('button', { name: '新建设计项目' }));
const dialog = screen.getByRole('dialog', { name: '新建设计项目' });
expect(screen.getAllByRole('dialog')).toHaveLength(1);
fireEvent.change(within(dialog).getByRole('textbox', { name: '项目名称' }), {
target: { value: ' 小狗的快乐短片 ' },
});
fireEvent.click(within(dialog).getByRole('button', { name: '创建项目' }));
await waitFor(() => expect(actions.createProject).toHaveBeenCalledExactlyOnceWith('小狗的快乐短片'));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(screen.getByLabelText('当前页面')).toHaveTextContent('/image-canvas');
expect(actions.selectProject).not.toHaveBeenCalled();
expect(actions.renameProject).not.toHaveBeenCalled();
expect(actions.deleteProject).not.toHaveBeenCalled();
});
it('ignores compact creation events while the desktop sidebar is hidden', () => {
const { actions } = prepareProjects();
setViewport(false);
renderSidebar({ hidden: true });
act(() => window.dispatchEvent(new Event(IMAGE_WORKSPACE_CREATE_PROJECT_EVENT)));
expect(document.querySelectorAll('[role="dialog"]')).toHaveLength(0);
expect(actions.createProject).not.toHaveBeenCalled();
expect(actions.selectProject).not.toHaveBeenCalled();
expect(screen.getByLabelText('当前页面')).toHaveTextContent('/module-select');
});
it('handles desktop creation events with a single dialog', () => {
prepareProjects();
renderSidebar();
act(() => window.dispatchEvent(new Event(IMAGE_WORKSPACE_CREATE_PROJECT_EVENT)));
expect(screen.getAllByRole('dialog')).toHaveLength(1);
expect(screen.getByRole('dialog', { name: '新建设计项目' })).toBeInTheDocument();
});
it('renames an unselected project through its keyboard menu without switching projects', async () => {
const { workspace, target, actions } = prepareProjects();
renderSidebar();
const menu = await openProjectMenu(target.title);
expect(within(menu).getByRole('menuitem', { name: '删除项目' })).toBeInTheDocument();
fireEvent.click(within(menu).getByRole('menuitem', { name: '重命名' }));
const dialog = await screen.findByRole('dialog', { name: '重命名设计项目' });
const name = within(dialog).getByRole('textbox', { name: '项目名称' });
expect(name).toHaveValue(target.title);
expect(actions.selectProject).not.toHaveBeenCalled();
expect(screen.getByLabelText('当前页面')).toHaveTextContent('/module-select');
fireEvent.change(name, { target: { value: ' ' } });
fireEvent.click(within(dialog).getByRole('button', { name: '保存名称' }));
expect(within(dialog).getByText('请输入项目名称')).toBeInTheDocument();
expect(actions.renameProject).not.toHaveBeenCalled();
fireEvent.change(name, { target: { value: ' 熊猫足球比赛 ' } });
fireEvent.click(within(dialog).getByRole('button', { name: '保存名称' }));
await waitFor(() => expect(actions.renameProject).toHaveBeenCalledExactlyOnceWith(target.workspaceId, '熊猫足球比赛'));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(useImageWorkspaceStore.getState().activeWorkspaceId).toBe(workspace.workspace.workspaceId);
expect(actions.selectProject).not.toHaveBeenCalled();
expect(actions.createProject).not.toHaveBeenCalled();
expect(actions.deleteProject).not.toHaveBeenCalled();
});
it('requires the full project title and deletes the menu target without switching projects', async () => {
const { workspace, target, actions } = prepareProjects();
renderSidebar();
const menu = await openProjectMenu(target.title);
fireEvent.click(within(menu).getByRole('menuitem', { name: '删除项目' }));
const dialog = await screen.findByRole('dialog', { name: '删除设计项目' });
const confirmation = within(dialog).getByRole('textbox', { name: `输入“${target.title}”确认` });
fireEvent.change(confirmation, { target: { value: `${target.title} ` } });
fireEvent.click(within(dialog).getByRole('button', { name: '删除项目' }));
expect(within(dialog).getByText('请输入完整项目名称')).toBeInTheDocument();
expect(actions.deleteProject).not.toHaveBeenCalled();
expect(actions.selectProject).not.toHaveBeenCalled();
fireEvent.change(confirmation, { target: { value: target.title } });
fireEvent.click(within(dialog).getByRole('button', { name: '删除项目' }));
await waitFor(() => expect(actions.deleteProject).toHaveBeenCalledExactlyOnceWith(target.workspaceId));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(useImageWorkspaceStore.getState().activeWorkspaceId).toBe(workspace.workspace.workspaceId);
expect(screen.getByLabelText('当前页面')).toHaveTextContent('/module-select');
expect(actions.selectProject).not.toHaveBeenCalled();
expect(actions.createProject).not.toHaveBeenCalled();
expect(actions.renameProject).not.toHaveBeenCalled();
});
it('shows only project titles under My Projects and keeps recent-first selection by workspace identity', () => {
const { workspace, target, actions } = prepareProjects();
renderSidebar();
const sidebar = screen.getByTestId('sidebar-image-workspace');
expect(within(sidebar).getByText('我的项目', { exact: true })).toBeInTheDocument();
expect(within(sidebar).queryByText('设计项目', { exact: true })).not.toBeInTheDocument();
expect(sidebar).not.toHaveTextContent('设计版本');
const activeProject = within(sidebar).getByRole('button', { name: workspace.workspace.title, exact: true });
const newerProject = within(sidebar).getByRole('button', { name: target.title, exact: true });
expect(activeProject.textContent).toBe(workspace.workspace.title);
expect(newerProject.textContent).toBe(target.title);
expect(activeProject).toHaveAttribute('aria-current', 'page');
// The September project is newer than the active August project, despite
// arriving second in bootstrap. Removing its date must not change order.
expect(newerProject.compareDocumentPosition(activeProject)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
fireEvent.click(newerProject);
expect(actions.selectProject).toHaveBeenCalledExactlyOnceWith(target.workspaceId);
expect(screen.getByLabelText('当前页面')).toHaveTextContent('/image-canvas');
expect(actions.renameProject).not.toHaveBeenCalled();
expect(actions.deleteProject).not.toHaveBeenCalled();
});
});