完善图片画布素材与项目封面交互

新增画布素材导出能力并补充 JSZip 依赖

优化素材上传占位进度、拖拽添加和文件夹移动交互

接入未登录项目访问弹窗并完善项目卡片封面缩略图

补充图片画布与项目页回归测试
This commit is contained in:
2026-06-16 16:15:15 +08:00
parent 80a382b034
commit 1d570605af
8 changed files with 2754 additions and 206 deletions

View File

@@ -2,8 +2,11 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ContextType } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ApiClientError } from '../../services/apiClient';
import { AuthUiContext } from '../auth/AuthUiContext';
import { ProjectGalleryView } from './ProjectGalleryView';
const listEditorProjectsMock = vi.hoisted(() => vi.fn());
@@ -11,6 +14,29 @@ const createEditorProjectMock = vi.hoisted(() => vi.fn());
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
const deleteEditorProjectMock = vi.hoisted(() => vi.fn());
type AuthValue = NonNullable<ContextType<typeof AuthUiContext>>;
function createAuthValue(overrides: Partial<AuthValue> = {}): AuthValue {
return {
user: null,
canAccessProtectedData: false,
openLoginModal: vi.fn(),
requireAuth: vi.fn((action: () => void) => action()),
openSettingsModal: vi.fn(),
openAccountModal: vi.fn(),
setCurrentUser: vi.fn(),
logout: vi.fn(),
musicVolume: 0.5,
setMusicVolume: vi.fn(),
platformTheme: 'light' as const,
setPlatformTheme: vi.fn(),
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
...overrides,
};
}
vi.mock('../../services/image-editor/editorProjectClient', () => ({
listEditorProjects: listEditorProjectsMock,
createEditorProject: createEditorProjectMock,
@@ -67,6 +93,115 @@ describe('ProjectGalleryView', () => {
expect(onOpenProject).toHaveBeenCalledWith('editor-project-1');
});
it('uses canvas-center layer composition as the project cover', async () => {
listEditorProjectsMock.mockResolvedValueOnce([
{
projectId: 'editor-project-cover',
title: '封面项目',
viewport: { x: 120, y: -60, scale: 0.8 },
layers: [
{
layerId: 'layer-cover-back',
resourceId: 'resource-back',
title: '背景图',
x: 200,
y: 160,
width: 300,
height: 180,
zIndex: 1,
},
{
layerId: 'layer-cover-front',
resourceId: 'resource-front',
title: '前景图',
x: 420,
y: 260,
width: 160,
height: 120,
zIndex: 2,
},
],
resources: [
{
resourceId: 'resource-front',
projectId: 'editor-project-cover',
imageSrc: 'data:image/png;base64,front',
width: 160,
height: 120,
sourceType: 'uploaded',
},
{
resourceId: 'resource-back',
projectId: 'editor-project-cover',
imageSrc: 'data:image/png;base64,back',
width: 300,
height: 180,
sourceType: 'uploaded',
},
],
updatedAt: '2026-06-12T08:00:00.000Z',
},
]);
render(<ProjectGalleryView onOpenProject={vi.fn()} />);
await screen.findByText('封面项目');
const cover = document.querySelector('.project-gallery__canvas-cover');
const coverLayers = document.querySelectorAll(
'.project-gallery__canvas-cover-layer',
);
expect(cover).toBeTruthy();
expect(coverLayers).toHaveLength(2);
expect(
document.querySelector('.project-gallery__preview img')?.getAttribute('src'),
).toBe('data:image/png;base64,back');
expect((coverLayers[0] as HTMLElement).style.zIndex).toBe('1');
expect((coverLayers[1] as HTMLElement).style.zIndex).toBe('2');
});
it('opens the login modal when project list loading is unauthorized', async () => {
const openLoginModal = vi.fn();
listEditorProjectsMock.mockRejectedValueOnce(
new ApiClientError({
message: '未授权访问',
status: 401,
code: 'UNAUTHORIZED',
}),
);
render(
<AuthUiContext.Provider value={createAuthValue({ openLoginModal })}>
<ProjectGalleryView onOpenProject={vi.fn()} />
</AuthUiContext.Provider>,
);
await waitFor(() => {
expect(openLoginModal).toHaveBeenCalledTimes(1);
});
expect(openLoginModal).toHaveBeenCalledWith(expect.any(Function));
expect(screen.queryByRole('alert')).toBeNull();
});
it('requires login before opening a project card while logged out', async () => {
const onOpenProject = vi.fn();
const requireAuth = vi.fn();
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
const user = userEvent.setup();
render(
<AuthUiContext.Provider value={createAuthValue({ requireAuth })}>
<ProjectGalleryView onOpenProject={onOpenProject} />
</AuthUiContext.Provider>,
);
await screen.findByText('角色设定板');
await user.click(screen.getByRole('button', { name: '打开项目角色设定板' }));
expect(requireAuth).toHaveBeenCalledWith(expect.any(Function));
expect(onOpenProject).not.toHaveBeenCalled();
});
it('renders project loading errors through the shared status message', async () => {
listEditorProjectsMock.mockRejectedValueOnce(new Error('读取项目失败'));