feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas, type FabricObject } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useAlign } from '../../src/hooks/use-align';
|
||||
|
||||
describe('useAlign', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
getActiveObject: vi.fn(),
|
||||
fire: vi.fn(),
|
||||
requestRenderAll: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockObject = (props = {}) => {
|
||||
const mockObject = {
|
||||
set: vi.fn(),
|
||||
setCoords: vi.fn(),
|
||||
getBoundingRect: vi.fn(() => ({
|
||||
width: 100,
|
||||
height: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
...props,
|
||||
})),
|
||||
width: 100,
|
||||
height: 100,
|
||||
...props,
|
||||
};
|
||||
return mockObject as unknown as FabricObject;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('alignLeft', () => {
|
||||
it('应该在 canvas 为 undefined 时不执行任何操作', () => {
|
||||
const { result } = renderHook(() => useAlign({ canvas: undefined }));
|
||||
act(() => {
|
||||
result.current.alignLeft();
|
||||
});
|
||||
// 由于没有 canvas,不应该有任何操作发生
|
||||
});
|
||||
|
||||
it('应该在选中对象少于 2 个时不执行任何操作', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: [createMockObject()] }),
|
||||
);
|
||||
act(() => {
|
||||
result.current.alignLeft();
|
||||
});
|
||||
expect(mockCanvas.fire).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确执行左对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignLeft();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
left: -mockActiveObject.width / 2,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignRight', () => {
|
||||
it('应该正确执行右对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignRight();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
left: mockActiveObject.width / 2 - obj.getBoundingRect().width,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignCenter', () => {
|
||||
it('应该正确执行水平居中对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignCenter();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
left: -obj.getBoundingRect().width / 2,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignTop', () => {
|
||||
it('应该正确执行顶部对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignTop();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
top: -mockActiveObject.height / 2,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignMiddle', () => {
|
||||
it('应该正确执行垂直居中对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignMiddle();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
top: -obj.getBoundingRect().height / 2,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignBottom', () => {
|
||||
it('应该正确执行底部对齐', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject();
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.alignBottom();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(obj.set).toHaveBeenCalledWith({
|
||||
top: mockActiveObject.height / 2 - obj.getBoundingRect().height,
|
||||
});
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verticalAverage', () => {
|
||||
it('应该正确执行水平均分', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject({ width: 300 });
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.verticalAverage();
|
||||
});
|
||||
|
||||
expect(mockObjects[0].set).toHaveBeenCalledWith({ left: -150 });
|
||||
expect(mockObjects[1].set).toHaveBeenCalledWith({ left: -50 });
|
||||
expect(mockObjects[2].set).toHaveBeenCalledWith({ left: 50 });
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizontalAverage', () => {
|
||||
it('应该正确执行垂直均分', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveObject = createMockObject({ height: 300 });
|
||||
const mockObjects = [
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
createMockObject(),
|
||||
];
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockActiveObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlign({ canvas: mockCanvas, selectObjects: mockObjects }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.horizontalAverage();
|
||||
});
|
||||
|
||||
expect(mockObjects[0].set).toHaveBeenCalledWith({ top: -150 });
|
||||
expect(mockObjects[1].set).toHaveBeenCalledWith({ top: -50 });
|
||||
expect(mockObjects[2].set).toHaveBeenCalledWith({ top: 50 });
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import type { FabricSchema } from '../../src/typings';
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useBackground } from '../../src/hooks/use-background';
|
||||
|
||||
// Mock ahooks
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounceEffect: (fn: () => void, deps: any[], options: any) => {
|
||||
useEffect(() => {
|
||||
fn();
|
||||
}, deps);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useBackground', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
backgroundColor: '#ffffff',
|
||||
set: vi.fn(),
|
||||
fire: vi.fn(),
|
||||
requestRenderAll: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockSchema = (backgroundColor = '#ffffff'): FabricSchema => ({
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: backgroundColor,
|
||||
backgroundColor,
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it('应该在 canvas 为 undefined 时不设置背景色', () => {
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useBackground({ canvas: undefined, schema }),
|
||||
);
|
||||
expect(result.current.backgroundColor).toBe('#ffffff');
|
||||
});
|
||||
|
||||
it('应该在初始化时从 canvas 获取背景色', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useBackground({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
expect(result.current.backgroundColor).toBe('#ffffff');
|
||||
});
|
||||
|
||||
it('应该在 schema 的背景色变化时更新背景色', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const initialSchema = createMockSchema('#ffffff');
|
||||
const { result, rerender } = renderHook(
|
||||
({ currentSchema }) =>
|
||||
useBackground({ canvas: mockCanvas, schema: currentSchema }),
|
||||
{
|
||||
initialProps: { currentSchema: initialSchema },
|
||||
},
|
||||
);
|
||||
|
||||
// 更新 schema
|
||||
const newSchema = createMockSchema('#000000');
|
||||
rerender({ currentSchema: newSchema });
|
||||
|
||||
// 等待 debounce
|
||||
await vi.runAllTimers();
|
||||
|
||||
expect(result.current.backgroundColor).toBe('#000000');
|
||||
expect(mockCanvas.set).toHaveBeenCalledWith({
|
||||
backgroundColor: '#000000',
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在背景色变化时更新 canvas', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useBackground({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setBackgroundColor('#000000');
|
||||
});
|
||||
|
||||
expect(mockCanvas.set).toHaveBeenCalledWith({
|
||||
backgroundColor: '#000000',
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在背景色相同时不重复更新 canvas', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const schema = createMockSchema('#ffffff');
|
||||
const { result } = renderHook(() =>
|
||||
useBackground({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setBackgroundColor('#ffffff');
|
||||
});
|
||||
|
||||
expect(mockCanvas.set).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.fire).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.requestRenderAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { type Canvas, type FabricObject } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { ViewVariableType } from '@coze-workflow/base/types';
|
||||
|
||||
import { createElement } from '../../src/utils';
|
||||
import type { FabricSchema } from '../../src/typings';
|
||||
import { useCanvasChange, saveProps } from '../../src/hooks/use-canvas-change';
|
||||
|
||||
enum Mode {
|
||||
INLINE_TEXT = 'inline_text',
|
||||
BLOCK_TEXT = 'block_text',
|
||||
RECT = 'rect',
|
||||
TRIANGLE = 'triangle',
|
||||
CIRCLE = 'ellipse',
|
||||
STRAIGHT_LINE = 'straight_line',
|
||||
PENCIL = 'pencil',
|
||||
IMAGE = 'img',
|
||||
GROUP = 'group',
|
||||
}
|
||||
|
||||
// Mock createElement
|
||||
vi.mock('../../src/utils', () => ({
|
||||
createElement: vi.fn(),
|
||||
defaultProps: {
|
||||
[Mode.IMAGE]: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
[Mode.BLOCK_TEXT]: {
|
||||
width: 200,
|
||||
height: 50,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock getUploadCDNAsset
|
||||
vi.mock('@coze-workflow/base-adapter', () => ({
|
||||
getUploadCDNAsset: vi.fn(path => `https://cdn.example.com${path}`),
|
||||
}));
|
||||
|
||||
describe('useCanvasChange', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
),
|
||||
off: vi.fn(),
|
||||
add: vi.fn(),
|
||||
setActiveObject: vi.fn(),
|
||||
toObject: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockObject = (props = {}) => {
|
||||
const mockObject = {
|
||||
customId: 'test-id',
|
||||
customType: 'test-type',
|
||||
...props,
|
||||
};
|
||||
return mockObject as unknown as FabricObject;
|
||||
};
|
||||
|
||||
const createMockSchema = (overrides = {}): FabricSchema => ({
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
customType: Mode.IMAGE,
|
||||
customId: 'canvas-1',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('应该在没有 canvas 时不设置事件监听', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: undefined,
|
||||
onChange: mockOnChange,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确监听画布变化事件', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockOnChange = vi.fn();
|
||||
const mockSchema = createMockSchema();
|
||||
(mockCanvas.toObject as Mock).mockReturnValue(mockSchema);
|
||||
|
||||
renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
onChange: mockOnChange,
|
||||
}),
|
||||
);
|
||||
|
||||
// 验证是否监听了所有默认事件
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:modified',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:added',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:removed',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:moving',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:modified-zIndex',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在事件触发时调用 onChange', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockOnChange = vi.fn();
|
||||
const mockSchema = createMockSchema();
|
||||
(mockCanvas.toObject as Mock).mockReturnValue(mockSchema);
|
||||
|
||||
renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
onChange: mockOnChange,
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:modified 事件的回调函数
|
||||
const modifiedCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:modified',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
modifiedCallback?.();
|
||||
|
||||
expect(mockCanvas.toObject).toHaveBeenCalledWith(saveProps);
|
||||
expect(mockOnChange).toHaveBeenCalledWith(mockSchema);
|
||||
});
|
||||
|
||||
it('应该在删除对象时重置引用关系', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockOnChange = vi.fn();
|
||||
const mockVariables = [
|
||||
{ id: 'var1', name: 'var1', type: ViewVariableType.String, index: 0 },
|
||||
{ id: 'var2', name: 'var2', type: ViewVariableType.String, index: 1 },
|
||||
];
|
||||
const mockSchema = createMockSchema({
|
||||
objects: [{ customId: 'obj1' }],
|
||||
customVariableRefs: [
|
||||
{ objectId: 'obj1', variableId: 'var1', variableName: 'var1' },
|
||||
{ objectId: 'obj2', variableId: 'var2', variableName: 'var2' },
|
||||
],
|
||||
});
|
||||
(mockCanvas.toObject as Mock).mockReturnValue(mockSchema);
|
||||
|
||||
renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
onChange: mockOnChange,
|
||||
schema: mockSchema,
|
||||
variables: mockVariables,
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:removed 事件的回调函数
|
||||
const removedCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:removed',
|
||||
)?.[1];
|
||||
|
||||
// 模拟删除事件
|
||||
removedCallback?.();
|
||||
|
||||
// 验证只保留了存在对象的引用
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
...mockSchema,
|
||||
customVariableRefs: [
|
||||
{ objectId: 'obj1', variableId: 'var1', variableName: 'var1' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确添加图片类型的变量引用', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockElement = createMockObject({ customId: 'img1' });
|
||||
vi.mocked(createElement).mockResolvedValue(mockElement);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
schema: createMockSchema(),
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.addRefObjectByVariable({
|
||||
id: 'var1',
|
||||
name: 'image1',
|
||||
type: ViewVariableType.Image,
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
|
||||
expect(createElement).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: Mode.IMAGE,
|
||||
elementProps: expect.objectContaining({
|
||||
editable: false,
|
||||
src: expect.stringContaining('img-placeholder.png'),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockCanvas.add).toHaveBeenCalledWith(mockElement);
|
||||
expect(mockCanvas.setActiveObject).toHaveBeenCalledWith(mockElement);
|
||||
});
|
||||
|
||||
it('应该正确添加文本类型的变量引用', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockElement = createMockObject({ customId: 'text1' });
|
||||
vi.mocked(createElement).mockResolvedValue(mockElement);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
schema: createMockSchema(),
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.addRefObjectByVariable({
|
||||
id: 'var1',
|
||||
name: 'text1',
|
||||
type: ViewVariableType.String,
|
||||
index: 0,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockCanvas.add).toHaveBeenCalledWith(mockElement);
|
||||
expect(mockCanvas.setActiveObject).toHaveBeenCalledWith(mockElement);
|
||||
});
|
||||
|
||||
it('应该正确更新对象的引用关系', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockVariables = [
|
||||
{ id: 'var1', name: 'var1', type: ViewVariableType.String, index: 0 },
|
||||
{ id: 'var2', name: 'var2', type: ViewVariableType.String, index: 1 },
|
||||
{ id: 'var3', name: 'var3', type: ViewVariableType.String, index: 2 },
|
||||
];
|
||||
const initialRefs = [
|
||||
{ objectId: 'obj1', variableId: 'var1', variableName: 'var1' },
|
||||
];
|
||||
const mockSchema = createMockSchema({ customVariableRefs: initialRefs });
|
||||
(mockCanvas.toObject as Mock).mockReturnValue({
|
||||
...mockSchema,
|
||||
customVariableRefs: [
|
||||
{ objectId: 'obj2', variableId: 'var3', variableName: 'var3' },
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasChange({
|
||||
canvas: mockCanvas,
|
||||
schema: createMockSchema({ customVariableRefs: initialRefs }),
|
||||
variables: mockVariables,
|
||||
}),
|
||||
);
|
||||
|
||||
// 更新已存在的引用
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj1',
|
||||
variable: {
|
||||
id: 'var2',
|
||||
name: 'var2',
|
||||
type: ViewVariableType.String,
|
||||
index: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 添加新的引用
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj2',
|
||||
variable: {
|
||||
id: 'var3',
|
||||
name: 'var3',
|
||||
type: ViewVariableType.String,
|
||||
index: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 删除引用
|
||||
act(() => {
|
||||
result.current.updateRefByObjectId({
|
||||
objectId: 'obj1',
|
||||
});
|
||||
});
|
||||
|
||||
// 验证最终的引用关系
|
||||
expect(mockCanvas.toObject().customVariableRefs).toEqual([
|
||||
{ objectId: 'obj2', variableId: 'var3', variableName: 'var3' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas, Rect } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import type { FabricSchema } from '../../src/typings';
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useCanvasClip } from '../../src/hooks/use-canvas-clip';
|
||||
|
||||
// Mock fabric
|
||||
vi.mock('fabric', () => ({
|
||||
Rect: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useCanvasClip', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
clipPath: undefined,
|
||||
requestRenderAll: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockSchema = (): FabricSchema => ({
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
backgroundColor: '#ffffff',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('addClip', () => {
|
||||
it('应该在 canvas 为 undefined 时不执行任何操作', () => {
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasClip({ canvas: undefined, schema }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.addClip();
|
||||
});
|
||||
|
||||
expect(Rect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确添加裁剪区域', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const schema = createMockSchema();
|
||||
const mockRect = {};
|
||||
(Rect as any).mockReturnValue(mockRect);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasClip({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.addClip();
|
||||
});
|
||||
|
||||
expect(Rect).toHaveBeenCalledWith({
|
||||
absolutePositioned: true,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: schema.width,
|
||||
height: schema.height,
|
||||
});
|
||||
expect(mockCanvas.clipPath).toBe(mockRect);
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeClip', () => {
|
||||
it('应该在 canvas 为 undefined 时不执行任何操作', () => {
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasClip({ canvas: undefined, schema }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.removeClip();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确移除裁剪区域', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const schema = createMockSchema();
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasClip({ canvas: mockCanvas, schema }),
|
||||
);
|
||||
|
||||
// 先添加裁剪区域
|
||||
act(() => {
|
||||
result.current.addClip();
|
||||
});
|
||||
|
||||
// 移除裁剪区域
|
||||
act(() => {
|
||||
result.current.removeClip();
|
||||
});
|
||||
|
||||
expect(mockCanvas.clipPath).toBeUndefined();
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type Canvas } from 'fabric';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useCanvasResize } from '../../src/hooks/use-canvas-resize';
|
||||
|
||||
describe('useCanvasResize', () => {
|
||||
const mockCanvas = {
|
||||
setDimensions: vi.fn(),
|
||||
} as unknown as Canvas;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should calculate correct scale', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 800,
|
||||
width: 500,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.scale).toBe(1.6);
|
||||
});
|
||||
|
||||
it('should calculate scale based on width constraint', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 800,
|
||||
maxHeight: 1000,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.scale).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should calculate scale based on height constraint', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 800,
|
||||
width: 500,
|
||||
height: 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.scale).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should not resize canvas when maxWidth is 0', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 0,
|
||||
maxHeight: 800,
|
||||
width: 500,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.resize(mockCanvas);
|
||||
expect(mockCanvas.setDimensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not resize canvas when maxHeight is 0', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 0,
|
||||
width: 500,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.resize(mockCanvas);
|
||||
expect(mockCanvas.setDimensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not resize canvas when canvas is undefined', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 800,
|
||||
width: 500,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.resize(undefined);
|
||||
expect(mockCanvas.setDimensions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set canvas dimensions correctly', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 800,
|
||||
width: 500,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.resize(mockCanvas);
|
||||
|
||||
// Check base dimensions
|
||||
expect(mockCanvas.setDimensions).toHaveBeenNthCalledWith(1, {
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
// Check CSS dimensions
|
||||
expect(mockCanvas.setDimensions).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
width: '800px',
|
||||
height: '800px',
|
||||
},
|
||||
{
|
||||
cssOnly: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should maintain aspect ratio when resizing', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCanvasResize({
|
||||
maxWidth: 1000,
|
||||
maxHeight: 800,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.resize(mockCanvas);
|
||||
|
||||
// Check CSS dimensions maintain aspect ratio
|
||||
expect(mockCanvas.setDimensions).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{
|
||||
width: '1000px',
|
||||
height: '500px',
|
||||
},
|
||||
{
|
||||
cssOnly: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas, type FabricObject } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useCommonOperation } from '../../src/hooks/use-common-operation';
|
||||
|
||||
describe('useCommonOperation', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
getActiveObject: vi.fn(),
|
||||
getActiveObjects: vi.fn(),
|
||||
discardActiveObject: vi.fn(),
|
||||
requestRenderAll: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
bringObjectToFront: vi.fn(),
|
||||
sendObjectToBack: vi.fn(),
|
||||
bringObjectForward: vi.fn(),
|
||||
sendObjectBackwards: vi.fn(),
|
||||
setWidth: vi.fn(),
|
||||
setHeight: vi.fn(),
|
||||
fire: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockObject = (props = {}) => {
|
||||
const mockObject = {
|
||||
isType: vi.fn(),
|
||||
fire: vi.fn(),
|
||||
set: vi.fn(),
|
||||
left: 100,
|
||||
top: 100,
|
||||
...props,
|
||||
};
|
||||
return mockObject as unknown as FabricObject;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('moveActiveObject', () => {
|
||||
it('应该在 canvas 为 undefined 时不执行任何操作', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: undefined }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveActiveObject('left');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确移动单个对象', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObject = createMockObject();
|
||||
(mockObject.isType as any).mockReturnValue(false);
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveActiveObject('left', 10);
|
||||
});
|
||||
|
||||
expect(mockObject.set).toHaveBeenCalledWith({ left: 90 });
|
||||
expect(mockObject.fire).toHaveBeenCalledWith('moving', {
|
||||
target: mockObject,
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确移动选中组', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObject = createMockObject();
|
||||
(mockObject.isType as any).mockReturnValue(true);
|
||||
(mockCanvas.getActiveObject as any).mockReturnValue(mockObject);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveActiveObject('right', 10);
|
||||
});
|
||||
|
||||
expect(mockObject.set).toHaveBeenCalledWith({ left: 110 });
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:moving', {
|
||||
target: mockObject,
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discardActiveObject', () => {
|
||||
it('应该正确取消选中对象', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.discardActiveObject();
|
||||
});
|
||||
|
||||
expect(mockCanvas.discardActiveObject).toHaveBeenCalled();
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeActiveObjects', () => {
|
||||
it('应该正确移除选中的对象', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObjects = [createMockObject(), createMockObject()];
|
||||
(mockCanvas.getActiveObjects as any).mockReturnValue(mockObjects);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.removeActiveObjects();
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(mockCanvas.remove).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
expect(mockCanvas.discardActiveObject).toHaveBeenCalled();
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveTo', () => {
|
||||
it('应该正确将对象移动到最前', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObjects = [createMockObject(), createMockObject()];
|
||||
(mockCanvas.getActiveObjects as any).mockReturnValue(mockObjects);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveTo('front');
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(mockCanvas.bringObjectToFront).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified-zIndex');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确将对象移动到最后', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObjects = [createMockObject(), createMockObject()];
|
||||
(mockCanvas.getActiveObjects as any).mockReturnValue(mockObjects);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveTo('backend');
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(mockCanvas.sendObjectToBack).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified-zIndex');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确将对象向前移动一层', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObjects = [createMockObject(), createMockObject()];
|
||||
(mockCanvas.getActiveObjects as any).mockReturnValue(mockObjects);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveTo('front-one');
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(mockCanvas.bringObjectForward).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified-zIndex');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确将对象向后移动一层', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockObjects = [createMockObject(), createMockObject()];
|
||||
(mockCanvas.getActiveObjects as any).mockReturnValue(mockObjects);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.moveTo('backend-one');
|
||||
});
|
||||
|
||||
mockObjects.forEach(obj => {
|
||||
expect(mockCanvas.sendObjectBackwards).toHaveBeenCalledWith(obj);
|
||||
});
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified-zIndex');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetWidthHeight', () => {
|
||||
it('应该正确重置画布宽度', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.resetWidthHeight({ width: 800 });
|
||||
});
|
||||
|
||||
expect(mockCanvas.setWidth).toHaveBeenCalledWith(800);
|
||||
expect(mockCanvas.setHeight).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确重置画布高度', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.resetWidthHeight({ height: 600 });
|
||||
});
|
||||
|
||||
expect(mockCanvas.setWidth).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.setHeight).toHaveBeenCalledWith(600);
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确同时重置画布宽度和高度', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const { result } = renderHook(() =>
|
||||
useCommonOperation({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.resetWidthHeight({ width: 800, height: 600 });
|
||||
});
|
||||
|
||||
expect(mockCanvas.setWidth).toHaveBeenCalledWith(800);
|
||||
expect(mockCanvas.setHeight).toHaveBeenCalledWith(600);
|
||||
expect(mockCanvas.fire).toHaveBeenCalledWith('object:modified');
|
||||
expect(mockCanvas.requestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { Mode, type FabricSchema } from '../../src/typings';
|
||||
import { useViewport } from '../../src/hooks/use-viewport';
|
||||
import { useSnapMove } from '../../src/hooks/use-snap-move';
|
||||
import { useRedoUndo } from '../../src/hooks/use-redo-undo';
|
||||
import { usePosition } from '../../src/hooks/use-position';
|
||||
import { useMousePosition } from '../../src/hooks/use-mouse-position';
|
||||
import { useInlineTextAdd } from '../../src/hooks/use-inline-text-add';
|
||||
import { useInitCanvas } from '../../src/hooks/use-init-canvas';
|
||||
import { useImagAdd } from '../../src/hooks/use-img-add';
|
||||
import { useGroup } from '../../src/hooks/use-group';
|
||||
import { useFreePencil } from '../../src/hooks/use-free-pencil';
|
||||
import { useFabricEditor } from '../../src/hooks/use-fabric-editor';
|
||||
import { useDragAdd } from '../../src/hooks/use-drag-add';
|
||||
import { useCopyPaste } from '../../src/hooks/use-copy-paste';
|
||||
import { useCommonOperation } from '../../src/hooks/use-common-operation';
|
||||
import { useCanvasResize } from '../../src/hooks/use-canvas-resize';
|
||||
import { useCanvasChange } from '../../src/hooks/use-canvas-change';
|
||||
import { useBackground } from '../../src/hooks/use-background';
|
||||
import { useAlign } from '../../src/hooks/use-align';
|
||||
import { useActiveObjectChange } from '../../src/hooks/use-active-object-change';
|
||||
|
||||
// Mock all dependencies
|
||||
vi.mock('../../src/hooks/use-canvas-resize', () => ({
|
||||
useCanvasResize: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-init-canvas', () => ({
|
||||
useInitCanvas: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-viewport', () => ({
|
||||
useViewport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-mouse-position', () => ({
|
||||
useMousePosition: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-group', () => ({
|
||||
useGroup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-canvas-change', () => ({
|
||||
useCanvasChange: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-snap-move', () => ({
|
||||
useSnapMove: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-copy-paste', () => ({
|
||||
useCopyPaste: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-redo-undo', () => ({
|
||||
useRedoUndo: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-active-object-change', () => ({
|
||||
useActiveObjectChange: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-align', () => ({
|
||||
useAlign: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-background', () => ({
|
||||
useBackground: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-common-operation', () => ({
|
||||
useCommonOperation: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-img-add', () => ({
|
||||
useImagAdd: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-drag-add', () => ({
|
||||
useDragAdd: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-free-pencil', () => ({
|
||||
useFreePencil: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-inline-text-add', () => ({
|
||||
useInlineTextAdd: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-position', () => ({
|
||||
usePosition: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useFabricEditor', () => {
|
||||
const mockRef = { current: document.createElement('canvas') };
|
||||
const mockSchema = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
};
|
||||
const mockMaxWidth = 1000;
|
||||
const mockMaxHeight = 800;
|
||||
const mockHelpLineLayerId = 'help-line-layer';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock returns
|
||||
(useCanvasResize as any).mockReturnValue({
|
||||
resize: vi.fn(),
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
(useInitCanvas as any).mockReturnValue({
|
||||
canvas: null,
|
||||
loadFromJSON: vi.fn(),
|
||||
});
|
||||
|
||||
(useViewport as any).mockReturnValue({
|
||||
viewport: { zoom: 1 },
|
||||
setViewport: vi.fn(),
|
||||
zoomToPoint: vi.fn(),
|
||||
});
|
||||
|
||||
(useMousePosition as any).mockReturnValue({
|
||||
mousePosition: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
(useGroup as any).mockReturnValue({
|
||||
group: vi.fn(),
|
||||
unGroup: vi.fn(),
|
||||
});
|
||||
|
||||
(useCanvasChange as any).mockReturnValue({
|
||||
startListen: vi.fn(),
|
||||
stopListen: vi.fn(),
|
||||
addRefObjectByVariable: vi.fn(),
|
||||
updateRefByObjectId: vi.fn(),
|
||||
customVariableRefs: [],
|
||||
});
|
||||
|
||||
(useSnapMove as any).mockImplementation(() => undefined);
|
||||
|
||||
(useCopyPaste as any).mockReturnValue({
|
||||
copy: vi.fn(),
|
||||
paste: vi.fn(),
|
||||
disabledPaste: false,
|
||||
});
|
||||
|
||||
(useRedoUndo as any).mockReturnValue({
|
||||
pushOperation: vi.fn(),
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
disabledRedo: false,
|
||||
disabledUndo: false,
|
||||
redoUndoing: false,
|
||||
});
|
||||
|
||||
(useActiveObjectChange as any).mockReturnValue({
|
||||
activeObjects: [],
|
||||
activeObjectsPopPosition: null,
|
||||
setActiveObjectsProps: vi.fn(),
|
||||
isActiveObjectsInBack: false,
|
||||
isActiveObjectsInFront: false,
|
||||
});
|
||||
|
||||
(useAlign as any).mockReturnValue({
|
||||
alignLeft: vi.fn(),
|
||||
alignRight: vi.fn(),
|
||||
alignCenter: vi.fn(),
|
||||
alignTop: vi.fn(),
|
||||
alignBottom: vi.fn(),
|
||||
alignMiddle: vi.fn(),
|
||||
verticalAverage: vi.fn(),
|
||||
horizontalAverage: vi.fn(),
|
||||
});
|
||||
|
||||
(useBackground as any).mockReturnValue({
|
||||
backgroundColor: '#ffffff',
|
||||
setBackgroundColor: vi.fn(),
|
||||
});
|
||||
|
||||
(useCommonOperation as any).mockReturnValue({
|
||||
moveActiveObject: vi.fn(),
|
||||
removeActiveObjects: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
discardActiveObject: vi.fn(),
|
||||
resetWidthHeight: vi.fn(),
|
||||
});
|
||||
|
||||
(useImagAdd as any).mockReturnValue({
|
||||
addImage: vi.fn(),
|
||||
});
|
||||
|
||||
(useDragAdd as any).mockReturnValue({
|
||||
enterDragAddElement: vi.fn(),
|
||||
exitDragAddElement: vi.fn(),
|
||||
});
|
||||
|
||||
(useFreePencil as any).mockReturnValue({
|
||||
enterFreePencil: vi.fn(),
|
||||
exitFreePencil: vi.fn(),
|
||||
});
|
||||
|
||||
(useInlineTextAdd as any).mockReturnValue({
|
||||
enterAddInlineText: vi.fn(),
|
||||
exitAddInlineText: vi.fn(),
|
||||
});
|
||||
|
||||
(usePosition as any).mockReturnValue({
|
||||
allObjectsPositionInScreen: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize with correct parameters', () => {
|
||||
renderHook(() =>
|
||||
useFabricEditor({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
helpLineLayerId: mockHelpLineLayerId,
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify useCanvasResize was called with correct params
|
||||
expect(useCanvasResize).toHaveBeenCalledWith({
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
width: mockSchema.width,
|
||||
height: mockSchema.height,
|
||||
});
|
||||
|
||||
// Verify useInitCanvas was called with correct params
|
||||
expect(useInitCanvas).toHaveBeenCalledWith({
|
||||
ref: mockRef.current,
|
||||
schema: mockSchema,
|
||||
startInit: true,
|
||||
readonly: false,
|
||||
resize: expect.any(Function),
|
||||
scale: 1,
|
||||
onClick: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle schema with legacy customVariableName', () => {
|
||||
const legacySchema = {
|
||||
...mockSchema,
|
||||
objects: [
|
||||
{
|
||||
customId: 'variable-img-123',
|
||||
customType: Mode.IMAGE,
|
||||
customVariableName: 'testVar',
|
||||
},
|
||||
{
|
||||
customId: 'variable-text-456',
|
||||
customType: Mode.INLINE_TEXT,
|
||||
customVariableName: 'testVar2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
renderHook(() =>
|
||||
useFabricEditor({
|
||||
ref: mockRef,
|
||||
schema: legacySchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
helpLineLayerId: mockHelpLineLayerId,
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify useInitCanvas was called with correct params
|
||||
expect(useInitCanvas).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ref: mockRef.current,
|
||||
startInit: true,
|
||||
readonly: false,
|
||||
scale: 1,
|
||||
onClick: undefined,
|
||||
schema: expect.objectContaining({
|
||||
objects: legacySchema.objects,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should enforce object limit', () => {
|
||||
const schemaWithObjects = {
|
||||
...mockSchema,
|
||||
objects: Array(50).fill({
|
||||
customId: 'test',
|
||||
customType: Mode.RECT,
|
||||
}),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFabricEditor({
|
||||
ref: mockRef,
|
||||
schema: schemaWithObjects,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
helpLineLayerId: mockHelpLineLayerId,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.state.couldAddNewObject).toBe(false);
|
||||
});
|
||||
|
||||
it('should return all required functions and states', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFabricEditor({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
helpLineLayerId: mockHelpLineLayerId,
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify the returned object structure
|
||||
expect(result.current).toEqual({
|
||||
canvas: null,
|
||||
canvasSettings: {
|
||||
width: mockSchema.width,
|
||||
height: mockSchema.height,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
sdk: expect.any(Object),
|
||||
state: expect.objectContaining({
|
||||
viewport: { zoom: 1 },
|
||||
cssScale: 1,
|
||||
activeObjects: [],
|
||||
activeObjectsPopPosition: null,
|
||||
couldAddNewObject: true,
|
||||
disabledPaste: false,
|
||||
disabledRedo: false,
|
||||
disabledUndo: false,
|
||||
redoUndoing: false,
|
||||
isActiveObjectsInBack: false,
|
||||
isActiveObjectsInFront: false,
|
||||
allObjectsPositionInScreen: [],
|
||||
canvasWidth: undefined,
|
||||
canvasHeight: undefined,
|
||||
customVariableRefs: [],
|
||||
objectLength: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onChange when canvas changes', () => {
|
||||
const onChange = vi.fn();
|
||||
const mockJson = { ...mockSchema };
|
||||
let changeCallback: ((json: FabricSchema) => void) | undefined;
|
||||
|
||||
(useCanvasChange as any).mockImplementation(
|
||||
({
|
||||
onChange: onChangeCallback,
|
||||
}: {
|
||||
onChange: (json: FabricSchema) => void;
|
||||
}) => {
|
||||
changeCallback = onChangeCallback;
|
||||
return {
|
||||
startListen: vi.fn(),
|
||||
stopListen: vi.fn(),
|
||||
addRefObjectByVariable: vi.fn(),
|
||||
updateRefByObjectId: vi.fn(),
|
||||
customVariableRefs: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() =>
|
||||
useFabricEditor({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
onChange,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
helpLineLayerId: mockHelpLineLayerId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (changeCallback) {
|
||||
changeCallback(mockJson);
|
||||
expect(onChange).toHaveBeenCalledWith(mockJson);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useSchemaChange } from '../../src/hooks/use-schema-change';
|
||||
import { useInitCanvas } from '../../src/hooks/use-init-canvas';
|
||||
import { useFabricPreview } from '../../src/hooks/use-fabric-preview';
|
||||
import { useCanvasResize } from '../../src/hooks/use-canvas-resize';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../src/hooks/use-canvas-resize', () => ({
|
||||
useCanvasResize: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-init-canvas', () => ({
|
||||
useInitCanvas: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/hooks/use-schema-change', () => ({
|
||||
useSchemaChange: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useFabricPreview', () => {
|
||||
const mockRef = { current: document.createElement('canvas') };
|
||||
const mockSchema = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
};
|
||||
const mockMaxWidth = 1000;
|
||||
const mockMaxHeight = 800;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock returns
|
||||
(useCanvasResize as any).mockReturnValue({
|
||||
resize: vi.fn(),
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
(useInitCanvas as any).mockReturnValue({
|
||||
canvas: null,
|
||||
});
|
||||
|
||||
(useSchemaChange as any).mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
it('should initialize with correct parameters', () => {
|
||||
renderHook(() =>
|
||||
useFabricPreview({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify useCanvasResize was called with correct params
|
||||
expect(useCanvasResize).toHaveBeenCalledWith({
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
width: mockSchema.width,
|
||||
height: mockSchema.height,
|
||||
});
|
||||
|
||||
// Verify useInitCanvas was called with correct params
|
||||
expect(useInitCanvas).toHaveBeenCalledWith({
|
||||
ref: mockRef.current,
|
||||
schema: mockSchema,
|
||||
startInit: true,
|
||||
readonly: true,
|
||||
resize: expect.any(Function),
|
||||
scale: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call resize when canvas is available', () => {
|
||||
const mockResize = vi.fn();
|
||||
const mockCanvas = {};
|
||||
|
||||
(useCanvasResize as any).mockReturnValue({
|
||||
resize: mockResize,
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
(useInitCanvas as any).mockReturnValue({
|
||||
canvas: mockCanvas,
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useFabricPreview({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockResize).toHaveBeenCalledWith(mockCanvas);
|
||||
});
|
||||
|
||||
it('should return correct state with cssScale', () => {
|
||||
const mockScale = 0.5;
|
||||
(useCanvasResize as any).mockReturnValue({
|
||||
resize: vi.fn(),
|
||||
scale: mockScale,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFabricPreview({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.state).toEqual({
|
||||
cssScale: mockScale,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call useSchemaChange with correct params', () => {
|
||||
const mockCanvas = {};
|
||||
(useInitCanvas as any).mockReturnValue({
|
||||
canvas: mockCanvas,
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useFabricPreview({
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
maxWidth: mockMaxWidth,
|
||||
maxHeight: mockMaxHeight,
|
||||
startInit: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(useSchemaChange).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
readonly: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
type Canvas,
|
||||
type ActiveSelection,
|
||||
type Group,
|
||||
type FabricObject,
|
||||
} from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { createElement } from '../../src/utils';
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useGroup } from '../../src/hooks/use-group';
|
||||
|
||||
// Mock createElement and isGroupElement
|
||||
const mockIsGroupElement = vi.fn();
|
||||
vi.mock('../../src/utils', () => ({
|
||||
createElement: vi.fn(),
|
||||
isGroupElement: (obj: unknown) => mockIsGroupElement(obj),
|
||||
}));
|
||||
|
||||
describe('useGroup', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
getActiveObject: vi.fn(),
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
setActiveObject: vi.fn(),
|
||||
discardActiveObject: vi.fn(),
|
||||
requestRenderAll: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
const createMockGroup = () => {
|
||||
const group = {
|
||||
type: 'group',
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 200,
|
||||
height: 200,
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
getObjects: vi.fn(),
|
||||
// 添加必要的 fabric.Object 属性
|
||||
noScaleCache: false,
|
||||
lockMovementX: false,
|
||||
lockMovementY: false,
|
||||
lockRotation: false,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
angle: 0,
|
||||
originX: 'left' as const,
|
||||
originY: 'top' as const,
|
||||
fill: '',
|
||||
stroke: '',
|
||||
strokeWidth: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
selectable: true,
|
||||
evented: true,
|
||||
canvas: null,
|
||||
};
|
||||
return group as unknown as Group;
|
||||
};
|
||||
|
||||
const createMockActiveSelection = () => {
|
||||
const selection = {
|
||||
getObjects: vi.fn(),
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 200,
|
||||
height: 200,
|
||||
};
|
||||
return selection as unknown as ActiveSelection;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(createElement).mockReset();
|
||||
mockIsGroupElement.mockReset();
|
||||
});
|
||||
|
||||
describe('group', () => {
|
||||
it('应该在没有 canvas 时不执行任何操作', async () => {
|
||||
const { result } = renderHook(() => useGroup({ canvas: undefined }));
|
||||
await act(() => result.current.group());
|
||||
expect(createElement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在选中少于两个元素时不执行分组', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveSelection = createMockActiveSelection();
|
||||
const mockGetObjects = vi.fn().mockReturnValue([{ id: '1' }]);
|
||||
Object.assign(mockActiveSelection, { getObjects: mockGetObjects });
|
||||
(
|
||||
mockCanvas.getActiveObject as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(mockActiveSelection);
|
||||
|
||||
const { result } = renderHook(() => useGroup({ canvas: mockCanvas }));
|
||||
await act(() => result.current.group());
|
||||
|
||||
expect(createElement).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.add).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确执行分组操作', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockActiveSelection = createMockActiveSelection();
|
||||
const mockObjects = [
|
||||
{ id: '1', type: 'rect' },
|
||||
{ id: '2', type: 'circle' },
|
||||
] as unknown as FabricObject[];
|
||||
const mockGroupElement = createMockGroup();
|
||||
|
||||
const mockGetObjects = vi.fn().mockReturnValue(mockObjects);
|
||||
Object.assign(mockActiveSelection, { getObjects: mockGetObjects });
|
||||
(
|
||||
mockCanvas.getActiveObject as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue(mockActiveSelection);
|
||||
vi.mocked(createElement).mockResolvedValue(mockGroupElement);
|
||||
|
||||
const { result } = renderHook(() => useGroup({ canvas: mockCanvas }));
|
||||
await act(() => result.current.group());
|
||||
|
||||
expect(createElement).toHaveBeenCalledWith({
|
||||
mode: Mode.GROUP,
|
||||
elementProps: {
|
||||
left: mockActiveSelection.left,
|
||||
top: mockActiveSelection.top,
|
||||
width: mockActiveSelection.width,
|
||||
height: mockActiveSelection.height,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockGroupElement.add).toHaveBeenCalledWith(...mockObjects);
|
||||
expect(mockCanvas.add).toHaveBeenCalledWith(mockGroupElement);
|
||||
expect(mockCanvas.setActiveObject).toHaveBeenCalledWith(mockGroupElement);
|
||||
expect(mockCanvas.remove).toHaveBeenCalledWith(...mockObjects);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unGroup', () => {
|
||||
it('应该在没有 canvas 时不执行任何操作', async () => {
|
||||
const { result } = renderHook(() => useGroup({ canvas: undefined }));
|
||||
await act(() => result.current.unGroup());
|
||||
expect(createElement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在选中的不是组元素时不执行解组', async () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
(
|
||||
mockCanvas.getActiveObject as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({ type: 'rect' });
|
||||
|
||||
const { result } = renderHook(() => useGroup({ canvas: mockCanvas }));
|
||||
await act(() => result.current.unGroup());
|
||||
|
||||
expect(createElement).not.toHaveBeenCalled();
|
||||
expect(mockCanvas.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Canvas } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { loadFontWithSchema, setElementAfterLoad } from '../../src/utils';
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useInitCanvas } from '../../src/hooks/use-init-canvas';
|
||||
|
||||
// Mock fabric
|
||||
vi.mock('fabric', () => ({
|
||||
Canvas: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock utils
|
||||
vi.mock('../../src/utils', () => ({
|
||||
loadFontWithSchema: vi.fn(),
|
||||
setElementAfterLoad: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useInitCanvas', () => {
|
||||
const mockRef = document.createElement('canvas');
|
||||
const mockSchema = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
backgroundColor: '#ffffff',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
};
|
||||
const mockResize = vi.fn();
|
||||
const mockLoadFromJSON = vi.fn();
|
||||
const mockRequestRenderAll = vi.fn();
|
||||
const mockDispose = vi.fn();
|
||||
const mockOn = vi.fn(() => () => {
|
||||
// 清理函数
|
||||
});
|
||||
let mockCanvas: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(window as any)._fabric_canvas = undefined;
|
||||
|
||||
mockCanvas = {
|
||||
loadFromJSON: mockLoadFromJSON,
|
||||
requestRenderAll: mockRequestRenderAll,
|
||||
dispose: mockDispose,
|
||||
on: mockOn,
|
||||
};
|
||||
|
||||
// Setup Canvas mock
|
||||
(Canvas as any).mockImplementation(() => mockCanvas);
|
||||
|
||||
// Mock loadFromJSON to resolve immediately
|
||||
mockLoadFromJSON.mockImplementation((json, callback) => {
|
||||
callback(null, {});
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not initialize canvas when startInit is false', () => {
|
||||
act(() => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: false,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(Canvas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not initialize canvas when ref is null', () => {
|
||||
act(() => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: null,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(Canvas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should initialize canvas with correct parameters', async () => {
|
||||
const scale = 1.5;
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
scale,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(Canvas).toHaveBeenCalledWith(mockRef, {
|
||||
width: mockSchema.width * scale,
|
||||
height: mockSchema.height * scale,
|
||||
backgroundColor: mockSchema.backgroundColor,
|
||||
selection: true,
|
||||
preserveObjectStacking: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call resize after canvas initialization', async () => {
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockResize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load schema and fonts after initialization', async () => {
|
||||
mockLoadFromJSON.mockImplementation((json, callback) => {
|
||||
callback(null, {});
|
||||
loadFontWithSchema({ schema: mockSchema, canvas: mockCanvas });
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockLoadFromJSON).toHaveBeenCalledWith(
|
||||
JSON.stringify(mockSchema),
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(loadFontWithSchema).toHaveBeenCalledWith({
|
||||
schema: mockSchema,
|
||||
canvas: mockCanvas,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set canvas in debug mode when not readonly', async () => {
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect((window as any)._fabric_canvas).toBe(mockCanvas);
|
||||
});
|
||||
|
||||
it('should not set canvas in debug mode when readonly', async () => {
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: true,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect((window as any)._fabric_canvas).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should bind click handler when provided', async () => {
|
||||
const mockOnClick = vi.fn();
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
onClick: mockOnClick,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockOn).toHaveBeenCalledWith('mouse:down', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should dispose canvas on unmount', async () => {
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
({ unmount } = renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
));
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
unmount();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(mockDispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle element loading callback', async () => {
|
||||
const mockElement = { type: 'rect' };
|
||||
mockLoadFromJSON.mockImplementation((json, callback) => {
|
||||
callback(null, mockElement);
|
||||
mockRequestRenderAll();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useInitCanvas({
|
||||
startInit: true,
|
||||
ref: mockRef,
|
||||
schema: mockSchema,
|
||||
readonly: false,
|
||||
resize: mockResize,
|
||||
}),
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(setElementAfterLoad).toHaveBeenCalledWith({
|
||||
element: mockElement,
|
||||
options: { readonly: false },
|
||||
canvas: expect.any(Object),
|
||||
});
|
||||
expect(mockRequestRenderAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas } from 'fabric';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useMousePosition } from '../../src/hooks/use-mouse-position';
|
||||
|
||||
describe('useMousePosition', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
),
|
||||
off: vi.fn(),
|
||||
getScenePoint: vi.fn(e => ({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('应该返回初始位置 {left: 0, top: 0}', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useMousePosition({ canvas: undefined }),
|
||||
);
|
||||
expect(result.current.mousePosition).toEqual({ left: 0, top: 0 });
|
||||
});
|
||||
|
||||
it('应该在没有 canvas 时不设置事件监听', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
renderHook(() => useMousePosition({ canvas: undefined }));
|
||||
expect(mockCanvas.on).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在有 canvas 时设置鼠标移动事件监听', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
renderHook(() => useMousePosition({ canvas: mockCanvas }));
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'mouse:move',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在鼠标移动时更新位置', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
let moveCallback: (event: {
|
||||
e: { clientX: number; clientY: number };
|
||||
}) => void;
|
||||
|
||||
const mockOn = vi.fn();
|
||||
Object.assign(mockCanvas, { on: mockOn });
|
||||
mockOn.mockImplementation((event: string, callback: any) => {
|
||||
if (event === 'mouse:move') {
|
||||
moveCallback = callback;
|
||||
}
|
||||
return () => {
|
||||
mockCanvas.off(event, callback);
|
||||
};
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMousePosition({ canvas: mockCanvas }),
|
||||
);
|
||||
|
||||
// 初始位置
|
||||
expect(result.current.mousePosition).toEqual({ left: 0, top: 0 });
|
||||
|
||||
// 模拟鼠标移动
|
||||
act(() => {
|
||||
moveCallback({
|
||||
e: { clientX: 100, clientY: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockCanvas.getScenePoint).toHaveBeenCalledWith({
|
||||
clientX: 100,
|
||||
clientY: 200,
|
||||
});
|
||||
expect(result.current.mousePosition).toEqual({ left: 100, top: 200 });
|
||||
});
|
||||
|
||||
it('应该在组件卸载时清理事件监听', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const cleanupSpy = vi.fn();
|
||||
const mockOn = vi.fn().mockReturnValue(cleanupSpy);
|
||||
Object.assign(mockCanvas, { on: mockOn });
|
||||
|
||||
const { unmount } = renderHook(() =>
|
||||
useMousePosition({ canvas: mockCanvas }),
|
||||
);
|
||||
unmount();
|
||||
|
||||
expect(cleanupSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在 canvas 变化时重新设置事件监听', () => {
|
||||
const mockCanvas1 = createMockCanvas();
|
||||
const mockCanvas2 = createMockCanvas();
|
||||
const cleanupSpy = vi.fn();
|
||||
const mockOn = vi.fn().mockReturnValue(cleanupSpy);
|
||||
Object.assign(mockCanvas1, { on: mockOn });
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ canvas }) => useMousePosition({ canvas }),
|
||||
{
|
||||
initialProps: { canvas: mockCanvas1 },
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockCanvas1.on).toHaveBeenCalledWith(
|
||||
'mouse:move',
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// 更新 canvas
|
||||
rerender({ canvas: mockCanvas2 });
|
||||
|
||||
// 应该清理旧的事件监听
|
||||
expect(cleanupSpy).toHaveBeenCalled();
|
||||
// 应该设置新的事件监听
|
||||
expect(mockCanvas2.on).toHaveBeenCalledWith(
|
||||
'mouse:move',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { type Canvas } from 'fabric';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { createSnap, snap } from '../../src/utils/snap/snap';
|
||||
import type { SnapService } from '../../src/utils/snap/snap';
|
||||
import { useSnapMove } from '../../src/hooks/use-snap-move';
|
||||
|
||||
// Mock snap utils
|
||||
vi.mock('../../src/utils/snap/snap', () => ({
|
||||
createSnap: vi.fn(() => ({
|
||||
move: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
helpline: {
|
||||
resetScale: vi.fn(),
|
||||
},
|
||||
canvas: null,
|
||||
threshold: 5,
|
||||
rules: [],
|
||||
snapX: vi.fn(),
|
||||
snapY: vi.fn(),
|
||||
snapPoints: [],
|
||||
snapLines: [],
|
||||
snapObjects: [],
|
||||
snapToObjects: vi.fn(),
|
||||
snapToPoints: vi.fn(),
|
||||
})),
|
||||
snap: {
|
||||
resetAllObjectsPosition: vi.fn(),
|
||||
helpline: {
|
||||
resetScale: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useSnapMove', () => {
|
||||
const createMockCanvas = () => {
|
||||
const mockCanvas = {
|
||||
on: vi.fn((event: string, callback: (event: any) => void) =>
|
||||
// 返回一个清理函数
|
||||
() => {
|
||||
mockCanvas.off(event, callback);
|
||||
},
|
||||
),
|
||||
off: vi.fn(),
|
||||
};
|
||||
return mockCanvas as unknown as Canvas;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('应该在没有 canvas 时不设置事件监听', () => {
|
||||
renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: undefined,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
expect(createSnap).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确初始化 snap 功能', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(createSnap).toHaveBeenCalledWith(mockCanvas, 'test-layer', 1);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'mouse:down',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'mouse:up',
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCanvas.on).toHaveBeenCalledWith(
|
||||
'object:moving',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在鼠标按下时重置所有对象位置', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 mouse:down 事件的回调函数
|
||||
const mouseDownCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'mouse:down',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
mouseDownCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(snap.resetAllObjectsPosition).toHaveBeenCalledWith({
|
||||
id: 'test-object',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在鼠标松开时重置 snap', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockSnap = {
|
||||
move: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
helpline: {
|
||||
resetScale: vi.fn(),
|
||||
},
|
||||
canvas: null,
|
||||
threshold: 5,
|
||||
rules: [],
|
||||
snapX: vi.fn(),
|
||||
snapY: vi.fn(),
|
||||
snapPoints: [],
|
||||
snapLines: [],
|
||||
snapObjects: [],
|
||||
snapToObjects: vi.fn(),
|
||||
snapToPoints: vi.fn(),
|
||||
} as unknown as SnapService;
|
||||
vi.mocked(createSnap).mockReturnValue(mockSnap);
|
||||
|
||||
renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 mouse:up 事件的回调函数
|
||||
const mouseUpCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'mouse:up',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
mouseUpCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(mockSnap.reset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在对象移动时调用 snap.move', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockSnap = {
|
||||
move: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
helpline: {
|
||||
resetScale: vi.fn(),
|
||||
},
|
||||
canvas: null,
|
||||
threshold: 5,
|
||||
rules: [],
|
||||
snapX: vi.fn(),
|
||||
snapY: vi.fn(),
|
||||
snapPoints: [],
|
||||
snapLines: [],
|
||||
snapObjects: [],
|
||||
snapToObjects: vi.fn(),
|
||||
snapToPoints: vi.fn(),
|
||||
} as unknown as SnapService;
|
||||
vi.mocked(createSnap).mockReturnValue(mockSnap);
|
||||
|
||||
renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
// 获取 object:moving 事件的回调函数
|
||||
const movingCallback = (mockCanvas.on as any).mock.calls.find(
|
||||
(call: [string, Function]) => call[0] === 'object:moving',
|
||||
)?.[1];
|
||||
|
||||
// 模拟事件触发
|
||||
movingCallback?.({ target: { id: 'test-object' } });
|
||||
|
||||
expect(mockSnap.move).toHaveBeenCalledWith({ id: 'test-object' });
|
||||
});
|
||||
|
||||
it('应该在组件卸载时销毁 snap', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
const mockSnap = {
|
||||
move: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
helpline: {
|
||||
resetScale: vi.fn(),
|
||||
},
|
||||
canvas: null,
|
||||
threshold: 5,
|
||||
rules: [],
|
||||
snapX: vi.fn(),
|
||||
snapY: vi.fn(),
|
||||
snapPoints: [],
|
||||
snapLines: [],
|
||||
snapObjects: [],
|
||||
snapToObjects: vi.fn(),
|
||||
snapToPoints: vi.fn(),
|
||||
} as unknown as SnapService;
|
||||
vi.mocked(createSnap).mockReturnValue(mockSnap);
|
||||
|
||||
const { unmount } = renderHook(() =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockSnap.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该在 scale 变化时重置辅助线比例', () => {
|
||||
const mockCanvas = createMockCanvas();
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ scale }) =>
|
||||
useSnapMove({
|
||||
canvas: mockCanvas,
|
||||
helpLineLayerId: 'test-layer',
|
||||
scale,
|
||||
}),
|
||||
{
|
||||
initialProps: { scale: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
// 更新 scale
|
||||
rerender({ scale: 2 });
|
||||
|
||||
expect(snap.helpline.resetScale).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type Canvas, type Point } from 'fabric';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { setViewport, zoomToPoint } from '../../src/utils';
|
||||
import { Mode } from '../../src/typings';
|
||||
import { useViewport } from '../../src/hooks/use-viewport';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../src/utils', () => ({
|
||||
setViewport: vi.fn(),
|
||||
zoomToPoint: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useViewport', () => {
|
||||
const mockSchema = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
background: '#ffffff',
|
||||
backgroundColor: '#ffffff',
|
||||
objects: [],
|
||||
customVariableRefs: [],
|
||||
customType: Mode.RECT,
|
||||
customId: 'test-canvas',
|
||||
};
|
||||
const mockFire = vi.fn();
|
||||
const mockCanvas = {
|
||||
fire: mockFire,
|
||||
} as unknown as Canvas;
|
||||
const minZoom = 0.3;
|
||||
const maxZoom = 3;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize with default viewport', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.viewport).toEqual([1, 0, 0, 1, 0, 0]);
|
||||
});
|
||||
|
||||
it('should not set viewport when canvas is undefined', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: undefined,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, 100, 100]);
|
||||
expect(setViewport).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should limit viewport x position to not move beyond left edge', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, 100, 0]);
|
||||
expect(setViewport).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
vpt: [2, 0, 0, 2, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit viewport x position to not move beyond right edge', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, -2000, 0]);
|
||||
expect(setViewport).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
vpt: [2, 0, 0, 2, -800, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit viewport y position to not move beyond top edge', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, 0, 100]);
|
||||
expect(setViewport).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
vpt: [2, 0, 0, 2, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit viewport y position to not move beyond bottom edge', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, 0, -2000]);
|
||||
expect(setViewport).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
vpt: [2, 0, 0, 2, 0, -600],
|
||||
});
|
||||
});
|
||||
|
||||
it('should fire object:moving event after setting viewport', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.setViewport([2, 0, 0, 2, 0, 0]);
|
||||
expect(mockFire).toHaveBeenCalledWith('object:moving');
|
||||
});
|
||||
|
||||
it('should call zoomToPoint with correct parameters', () => {
|
||||
const mockPoint: Point = { x: 100, y: 100 };
|
||||
const mockZoomLevel = 2;
|
||||
const mockVpt = [2, 0, 0, 2, 0, 0];
|
||||
|
||||
(zoomToPoint as jest.Mock).mockReturnValue(mockVpt);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useViewport({
|
||||
canvas: mockCanvas,
|
||||
schema: mockSchema,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.zoomToPoint(mockPoint, mockZoomLevel);
|
||||
|
||||
expect(zoomToPoint).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
point: mockPoint,
|
||||
zoomLevel: mockZoomLevel,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
});
|
||||
|
||||
expect(setViewport).toHaveBeenCalledWith({
|
||||
canvas: mockCanvas,
|
||||
vpt: mockVpt,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user