Files
Genarrative/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx
T
JenkenB 097170836d Add UI-design asset extraction & spritesheet support
Introduce a new UI design asset extraction flow: add POST /api/editor/ui-designs/assets/extractions handler that always uses gpt-image-2 with the fixed prompt "提取画面中的所有独立并整理成spritesheet"; parse Data URL references, call OpenAI image edits, return a spritesheet and sliced icon assets. Increase request body limit for image Data URLs and wire the route with bearer auth. Add platform-image slicing: slice_generated_icon_spritesheet_all_by_connected_components (with auto-naming "素材 1..N", foreground estimation and heuristics) and export it. Add Rust tests for the handler, prompt constant, and slicing behavior. Update frontend client and integration tests to call the new extractEditorUiDesignAssets API and verify spritesheet + icon layers are added to the canvas and persisted. Update multiple docs to describe the extraction UI, toolbar placement, and related audio/playback pitfalls. Minor related updates: toolbar button ordering and audio duration / signed read-url notes.
2026-06-19 10:09:19 +08:00

170 lines
5.5 KiB
TypeScript

/* @vitest-environment jsdom */
import {
cleanup,
fireEvent,
render,
screen,
within,
} from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasSelectedLayerToolbarView } from './ImageCanvasSelectedLayerToolbarView';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return {
id: 'layer-1',
resourceId: 'resource-1',
title: '生成主图',
src: 'data:image/png;base64,layer',
x: 0,
y: 0,
width: 320,
height: 240,
originalWidth: 320,
originalHeight: 240,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function renderSelectedToolbar(
overrides: Partial<Parameters<typeof ImageCanvasSelectedLayerToolbarView>[0]> = {},
) {
const props: Parameters<typeof ImageCanvasSelectedLayerToolbarView>[0] = {
selectedLayer: createLayer(),
selectedToolbarStyle: { left: 12, top: 24 },
onOpenQuickEditPanel: vi.fn(),
onOpenRedrawPanel: vi.fn(),
onOpenCropExpandPanel: vi.fn(),
onRemoveBackground: vi.fn(),
onExtractUiDesignAssets: vi.fn(),
onOpenCharacterAnimationPanel: vi.fn(),
onDownloadLayer: vi.fn(),
...overrides,
};
render(<ImageCanvasSelectedLayerToolbarView {...props} />);
return props;
}
describe('ImageCanvasSelectedLayerToolbarView', () => {
it('renders common layer actions in the Lovart toolbar order and forwards callbacks', () => {
const props = renderSelectedToolbar();
const toolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
const buttons = within(toolbar).getAllByRole('button');
expect(buttons.map((button) => button.getAttribute('aria-label'))).toEqual([
'快速编辑',
'裁扩按钮',
'去除背景按钮',
'重绘',
'下载按钮',
]);
fireEvent.click(within(toolbar).getByRole('button', { name: '快速编辑' }));
fireEvent.click(within(toolbar).getByRole('button', { name: '裁扩按钮' }));
fireEvent.click(
within(toolbar).getByRole('button', { name: '去除背景按钮' }),
);
fireEvent.click(within(toolbar).getByRole('button', { name: '重绘' }));
fireEvent.click(within(toolbar).getByRole('button', { name: '下载按钮' }));
expect(props.onOpenQuickEditPanel).toHaveBeenCalledWith(props.selectedLayer);
expect(props.onOpenCropExpandPanel).toHaveBeenCalledWith(
props.selectedLayer,
);
expect(props.onRemoveBackground).toHaveBeenCalledWith(props.selectedLayer);
expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(props.selectedLayer);
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
expect(
within(toolbar).queryByRole('button', { name: '删除图片' }),
).toBeNull();
expect(
within(toolbar).queryByRole('button', { name: '修改图片' }),
).toBeNull();
expect(
within(toolbar).queryByRole('button', { name: /查看.*图片信息/u }),
).toBeNull();
expect(
within(toolbar).queryByRole('button', { name: '提取素材' }),
).toBeNull();
});
it('renders UI design asset extraction after remove background', () => {
const layer = createLayer({
sourceType: 'generated',
assetKind: 'ui-design',
});
const props = renderSelectedToolbar({ selectedLayer: layer });
const toolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
const buttons = within(toolbar).getAllByRole('button');
expect(buttons.map((button) => button.getAttribute('aria-label'))).toEqual([
'快速编辑',
'裁扩按钮',
'去除背景按钮',
'提取素材',
'重绘',
'下载按钮',
]);
fireEvent.click(within(toolbar).getByRole('button', { name: '提取素材' }));
expect(props.onExtractUiDesignAssets).toHaveBeenCalledWith(layer);
});
it('renders character animation only for character layers', () => {
const layer = createLayer({
sourceType: 'generated',
assetKind: 'character',
});
const props = renderSelectedToolbar({ selectedLayer: layer });
fireEvent.click(screen.getByRole('button', { name: '生成动画' }));
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
cleanup();
renderSelectedToolbar({ selectedLayer: createLayer({ id: 'layer-2' }) });
expect(screen.queryByRole('button', { name: '生成动画' })).toBeNull();
});
it('renders nothing without a selected layer or toolbar position', () => {
const { rerender } = render(
<ImageCanvasSelectedLayerToolbarView
selectedLayer={null}
selectedToolbarStyle={{ left: 0, top: 0 }}
onOpenQuickEditPanel={vi.fn()}
onOpenRedrawPanel={vi.fn()}
onOpenCropExpandPanel={vi.fn()}
onRemoveBackground={vi.fn()}
onExtractUiDesignAssets={vi.fn()}
onOpenCharacterAnimationPanel={vi.fn()}
onDownloadLayer={vi.fn()}
/>,
);
expect(screen.queryByRole('toolbar', { name: '图片工具栏' })).toBeNull();
rerender(
<ImageCanvasSelectedLayerToolbarView
selectedLayer={createLayer()}
selectedToolbarStyle={null}
onOpenQuickEditPanel={vi.fn()}
onOpenRedrawPanel={vi.fn()}
onOpenCropExpandPanel={vi.fn()}
onRemoveBackground={vi.fn()}
onExtractUiDesignAssets={vi.fn()}
onOpenCharacterAnimationPanel={vi.fn()}
onDownloadLayer={vi.fn()}
/>,
);
expect(screen.queryByRole('toolbar', { name: '图片工具栏' })).toBeNull();
});
});