拆分编辑器素材集成测试

新增编辑器测试共享工具

迁出素材库和上传集成用例

保留主编辑器集成测试关键链路

更新 TRACKING.md 记录第三十九阶段验证
This commit is contained in:
2026-06-17 18:45:48 +08:00
parent bf24d259a7
commit f6e272e612
4 changed files with 1272 additions and 1105 deletions
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,342 @@
import JSZip from 'jszip';
import type { ContextType } from 'react';
import { fireEvent } from '@testing-library/react';
import { afterEach, beforeEach, expect, type Mock, vi } from 'vitest';
import { ApiClientError } from '../../services/apiClient';
import { AuthUiContext } from '../auth/AuthUiContext';
import { ImageCanvasEditorView } from './ImageCanvasEditorView';
export { ApiClientError, AuthUiContext, ImageCanvasEditorView };
export type AuthValue = NonNullable<
ContextType<typeof AuthUiContext>
>;
export type ImageCanvasEditorViewServiceMocks = {
generateEditorImageMock: Mock;
generateEditorIconSpritesheetMock: Mock;
generateEditorCharacterAnimationMock: Mock;
editEditorImageMock: Mock;
createEditorAssetMock: Mock;
createEditorProjectResourceMock: Mock;
createEditorAssetFolderMock: Mock;
updateEditorAssetMock: Mock;
updateEditorAssetFolderMock: Mock;
deleteEditorAssetFolderMock: Mock;
deleteEditorAssetMock: Mock;
loadEditorAssetLibraryMock: Mock;
loadEditorProjectMock: Mock;
loadOrCreateRecentEditorProjectMock: Mock;
renameEditorProjectMock: Mock;
saveEditorProjectLayoutMock: Mock;
};
const runRequiredAuthAction = (action: () => void) => action();
export function createAuthValue(overrides: Partial<AuthValue> = {}): AuthValue {
return {
user: null,
canAccessProtectedData: false,
openLoginModal: vi.fn(),
requireAuth: vi.fn(runRequiredAuthAction),
openSettingsModal: vi.fn(),
openAccountModal: vi.fn(),
setCurrentUser: vi.fn(),
logout: vi.fn(),
musicVolume: 0.5,
setMusicVolume: vi.fn(),
platformTheme: 'light',
setPlatformTheme: vi.fn(),
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
...overrides,
};
}
export const defaultEditorProjectResources = [
{
resourceId: 'resource-puzzle',
projectId: 'editor-project-default',
imageSrc: '/creation-type-references/puzzle.webp',
width: 640,
height: 640,
sourceType: 'uploaded',
},
{
resourceId: 'resource-big-fish',
projectId: 'editor-project-default',
imageSrc: '/creation-type-references/big-fish.webp',
width: 720,
height: 405,
sourceType: 'uploaded',
},
];
export const defaultEditorProjectLayers = [
{
layerId: 'layer-puzzle',
resourceId: 'resource-puzzle',
title: '拼图素材',
x: 470,
y: 300,
width: 640,
height: 640,
originalWidth: 640,
originalHeight: 640,
zIndex: 1,
sourceType: 'uploaded',
},
{
layerId: 'layer-big-fish',
resourceId: 'resource-big-fish',
title: '大鱼素材',
x: 930,
y: 360,
width: 720,
height: 405,
originalWidth: 720,
originalHeight: 405,
zIndex: 2,
sourceType: 'uploaded',
},
];
export const defaultEditorAssetLibraryAssets = [
{
assetId: 'asset-puzzle',
folderId: 'project',
label: '拼图素材',
imageSrc: '/creation-type-references/puzzle.webp',
width: 640,
height: 640,
sourceType: 'uploaded',
},
{
assetId: 'asset-match3d',
folderId: 'project',
label: '抓大鹅素材',
imageSrc: '/creation-type-references/match3d.webp',
width: 640,
height: 640,
sourceType: 'uploaded',
},
{
assetId: 'asset-big-fish',
folderId: 'project',
label: '大鱼素材',
imageSrc: '/creation-type-references/big-fish.webp',
width: 720,
height: 405,
sourceType: 'uploaded',
},
{
assetId: 'asset-bark-battle',
folderId: 'project',
label: '声浪素材',
imageSrc: '/creation-type-references/bark-battle.webp',
width: 640,
height: 900,
sourceType: 'uploaded',
},
{
assetId: 'asset-visual-novel',
folderId: 'project',
label: '视觉小说素材',
imageSrc: '/creation-type-references/visual-novel.webp',
width: 720,
height: 405,
sourceType: 'uploaded',
},
];
export function dispatchPointerEvent(
target: Element,
type: string,
init: MouseEventInit & { pointerId: number },
) {
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
...init,
});
Object.defineProperty(event, 'pointerId', { value: init.pointerId });
fireEvent(target, event);
}
export function immediateAsync<T>(value: T) {
return {
then(onFulfilled: (value: T) => unknown) {
onFulfilled(value);
return {
catch() {},
};
},
};
}
export function createDataTransferStub() {
const store = new Map<string, string>();
return {
files: [],
types: [] as string[],
dropEffect: 'none',
effectAllowed: 'all',
setData(type: string, value: string) {
store.set(type, value);
if (!this.types.includes(type)) {
this.types.push(type);
}
},
getData(type: string) {
return store.get(type) ?? '';
},
};
}
export function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, resolve, reject };
}
export async function readZipText(zip: JSZip, path: string) {
const file = zip.file(path);
expect(file).toBeTruthy();
return file!.async('string');
}
export function setupImageCanvasEditorViewTestLifecycle({
generateEditorImageMock,
generateEditorIconSpritesheetMock,
generateEditorCharacterAnimationMock,
editEditorImageMock,
createEditorAssetMock,
createEditorProjectResourceMock,
createEditorAssetFolderMock,
updateEditorAssetMock,
updateEditorAssetFolderMock,
deleteEditorAssetFolderMock,
deleteEditorAssetMock,
loadEditorAssetLibraryMock,
loadEditorProjectMock,
loadOrCreateRecentEditorProjectMock,
renameEditorProjectMock,
saveEditorProjectLayoutMock,
}: ImageCanvasEditorViewServiceMocks) {
beforeEach(() => {
loadOrCreateRecentEditorProjectMock.mockImplementation(() =>
immediateAsync({
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
}),
);
loadEditorAssetLibraryMock.mockImplementation(() =>
immediateAsync({
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: defaultEditorAssetLibraryAssets,
}),
);
createEditorAssetMock.mockImplementation(async (input) => ({
assetId: `persisted-${input.label}`,
folderId: input.folderId,
label: input.label,
imageSrc: input.imageSrc,
width: input.width,
height: input.height,
sourceType: input.sourceType,
}));
createEditorAssetFolderMock.mockResolvedValue({
folderId: 'folder-role-persisted',
label: '角色上传',
collapsed: false,
systemDefault: false,
});
updateEditorAssetMock.mockImplementation(async (assetId, input) => ({
assetId,
folderId: input.folderId ?? 'project',
label: input.label ?? '拼图素材',
imageSrc: '/creation-type-references/puzzle.webp',
width: 640,
height: 640,
sourceType: 'uploaded',
}));
renameEditorProjectMock.mockImplementation(async (projectId, title) => ({
projectId,
title,
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
}));
updateEditorAssetFolderMock.mockImplementation(async (folderId, input) => ({
folderId,
label: input.label ?? '角色上传',
collapsed: input.collapsed ?? false,
systemDefault: false,
}));
deleteEditorAssetFolderMock.mockResolvedValue({
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: [],
});
deleteEditorAssetMock.mockResolvedValue({});
createEditorProjectResourceMock.mockImplementation(
async (projectId, input) => ({
resourceId: `resource-${projectId}-${input.width}`,
projectId,
imageSrc: input.imageSrc,
width: input.width,
height: input.height,
sourceType: input.sourceType,
}),
);
saveEditorProjectLayoutMock.mockResolvedValue({});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
generateEditorImageMock.mockReset();
generateEditorIconSpritesheetMock.mockReset();
generateEditorCharacterAnimationMock.mockReset();
editEditorImageMock.mockReset();
createEditorAssetMock.mockReset();
createEditorProjectResourceMock.mockReset();
createEditorAssetFolderMock.mockReset();
updateEditorAssetMock.mockReset();
updateEditorAssetFolderMock.mockReset();
deleteEditorAssetFolderMock.mockReset();
deleteEditorAssetMock.mockReset();
loadEditorAssetLibraryMock.mockReset();
loadEditorProjectMock.mockReset();
loadOrCreateRecentEditorProjectMock.mockReset();
renameEditorProjectMock.mockReset();
saveEditorProjectLayoutMock.mockReset();
window.history.replaceState(null, '', '/editor/canvas');
});
}
File diff suppressed because it is too large Load Diff