/* @vitest-environment jsdom */ import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { ProjectGalleryView } from './ProjectGalleryView'; const listEditorProjectsMock = vi.hoisted(() => vi.fn()); const createEditorProjectMock = vi.hoisted(() => vi.fn()); const renameEditorProjectMock = vi.hoisted(() => vi.fn()); const deleteEditorProjectMock = vi.hoisted(() => vi.fn()); vi.mock('../../services/image-editor/editorProjectClient', () => ({ listEditorProjects: listEditorProjectsMock, createEditorProject: createEditorProjectMock, renameEditorProject: renameEditorProjectMock, deleteEditorProject: deleteEditorProjectMock, })); const projectItems = [ { projectId: 'editor-project-1', title: '角色设定板', viewport: { x: 0, y: 0, scale: 1 }, layers: [{ layerId: 'layer-1', resourceId: 'resource-1' }], resources: [ { resourceId: 'resource-1', projectId: 'editor-project-1', imageSrc: 'data:image/png;base64,one', width: 1024, height: 1024, sourceType: 'uploaded', }, ], updatedAt: '2026-06-12T08:00:00.000Z', }, { projectId: 'editor-project-2', title: '场景草图', viewport: { x: 0, y: 0, scale: 1 }, layers: [], resources: [], updatedAt: '2026-06-12T07:00:00.000Z', }, ]; describe('ProjectGalleryView', () => { afterEach(() => { listEditorProjectsMock.mockReset(); createEditorProjectMock.mockReset(); renameEditorProjectMock.mockReset(); deleteEditorProjectMock.mockReset(); }); it('opens a project from the gallery card', async () => { const onOpenProject = vi.fn(); listEditorProjectsMock.mockResolvedValueOnce(projectItems); const user = userEvent.setup(); render(); await screen.findByText('角色设定板'); await user.click(screen.getByRole('button', { name: '打开项目角色设定板' })); expect(onOpenProject).toHaveBeenCalledWith('editor-project-1'); }); it('renames and deletes a project from the hover menu', async () => { listEditorProjectsMock.mockResolvedValueOnce(projectItems); renameEditorProjectMock.mockResolvedValueOnce({ ...projectItems[0], title: '新角色设定板', }); deleteEditorProjectMock.mockResolvedValueOnce('editor-project-2'); const user = userEvent.setup(); render(); await screen.findByText('角色设定板'); await user.click(screen.getByRole('button', { name: '打开项目角色设定板菜单' })); await user.click(screen.getByRole('menuitem', { name: /重命名/u })); await user.clear(screen.getByLabelText('项目名称')); await user.type(screen.getByLabelText('项目名称'), '新角色设定板'); await user.click(screen.getByRole('button', { name: '保存' })); expect(renameEditorProjectMock).toHaveBeenCalledWith( 'editor-project-1', '新角色设定板', ); await screen.findByText('新角色设定板'); await user.click(screen.getByRole('button', { name: '打开项目场景草图菜单' })); await user.click(screen.getByRole('menuitem', { name: /删除/u })); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-2'); await waitFor(() => { expect(screen.queryByText('场景草图')).toBeNull(); }); }); it('supports batch selection actions from the bottom toolbar', async () => { listEditorProjectsMock.mockResolvedValueOnce(projectItems); deleteEditorProjectMock.mockResolvedValue('deleted'); const user = userEvent.setup(); render(); await screen.findByText('角色设定板'); await user.click(screen.getByRole('button', { name: '选择' })); const toolbar = screen.getByRole('toolbar', { name: '批量操作' }); await user.click(within(toolbar).getByRole('button', { name: '全选' })); expect( within(toolbar).getByRole('button', { name: '取消全选 · 已选 2' }), ).toBeTruthy(); await user.click(within(toolbar).getByRole('button', { name: '删除' })); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-1'); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-2'); }); });