拆分编辑器素材集成测试
新增编辑器测试共享工具 迁出素材库和上传集成用例 保留主编辑器集成测试关键链路 更新 TRACKING.md 记录第三十九阶段验证
This commit is contained in:
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
Reference in New Issue
Block a user