5a884f1d11
让生成参考图和角色动作参考图通过 objectKey 换签显示 让视频图层 poster 与框选预览复用签名读取 补充参考槽、框选预览和视频图层回归测试 沉淀框选预览换签缓存踩坑
293 lines
8.6 KiB
TypeScript
293 lines
8.6 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
import type { ImgHTMLAttributes } from 'react';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
|
import type { UiAssetExtractionState } from './ImageCanvasUiAssetExtractionModel';
|
|
import { ImageCanvasUiAssetExtractionOverlayView } from './ImageCanvasUiAssetExtractionOverlayView';
|
|
|
|
type ResolvedAssetImageMockProps = Omit<
|
|
ImgHTMLAttributes<HTMLImageElement>,
|
|
'src'
|
|
> & {
|
|
src?: string | null;
|
|
objectKey?: string | null;
|
|
fallbackSrc?: string | null;
|
|
refreshKey?: string | number | null;
|
|
};
|
|
|
|
vi.mock('../ResolvedAssetImage', () => ({
|
|
ResolvedAssetImage: ({
|
|
src,
|
|
objectKey,
|
|
fallbackSrc,
|
|
refreshKey,
|
|
...props
|
|
}: ResolvedAssetImageMockProps) => (
|
|
<img
|
|
{...props}
|
|
src={src ?? fallbackSrc ?? ''}
|
|
data-object-key={objectKey ?? ''}
|
|
data-fallback-src={fallbackSrc ?? ''}
|
|
data-refresh-key={refreshKey == null ? '' : String(refreshKey)}
|
|
/>
|
|
),
|
|
}));
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-source',
|
|
resourceId: 'resource-source',
|
|
title: '源图',
|
|
src: 'data:image/png;base64,source',
|
|
x: 100,
|
|
y: 120,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 640,
|
|
originalHeight: 480,
|
|
zIndex: 4,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createState(
|
|
overrides: Partial<UiAssetExtractionState> = {},
|
|
): UiAssetExtractionState {
|
|
return {
|
|
sourceLayerId: 'layer-source',
|
|
tool: 'rect',
|
|
marks: [],
|
|
draftMark: null,
|
|
status: 'idle',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('ImageCanvasUiAssetExtractionOverlayView', () => {
|
|
it('renders quick edit tools vertically on the right without extraction actions', () => {
|
|
const handleToolChange = vi.fn();
|
|
|
|
render(
|
|
<ImageCanvasUiAssetExtractionOverlayView
|
|
variant="quick-edit"
|
|
sourceLayer={createLayer()}
|
|
viewport={{ x: 10, y: 20, scale: 1 }}
|
|
state={createState()}
|
|
onToolChange={handleToolChange}
|
|
onPointerStart={vi.fn()}
|
|
onPointerMove={vi.fn()}
|
|
onPointerEnd={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const toolbar = screen.getByRole('toolbar', {
|
|
name: '快速编辑框选工具',
|
|
});
|
|
|
|
expect(toolbar.className).toContain(
|
|
'image-canvas-editor__ui-extraction-toolbar--right',
|
|
);
|
|
expect(within(toolbar).queryByText('框选素材')).toBeNull();
|
|
expect(within(toolbar).queryByText('0')).toBeNull();
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '提取' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(toolbar).getByRole('button', { name: '矩形框选' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(toolbar).getByRole('button', { name: '椭圆框选' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(toolbar)
|
|
.getByRole('button', { name: '矩形框选' })
|
|
.getAttribute('aria-pressed'),
|
|
).toBe('true');
|
|
fireEvent.click(
|
|
within(toolbar).getByRole('button', { name: '矩形框选' }),
|
|
);
|
|
fireEvent.click(
|
|
within(toolbar).getByRole('button', { name: '画笔自由框选' }),
|
|
);
|
|
|
|
expect(handleToolChange).toHaveBeenNthCalledWith(1, null);
|
|
expect(handleToolChange).toHaveBeenNthCalledWith(2, 'brush');
|
|
});
|
|
|
|
it('renders UI extraction tools on the right and a bottom composer with previews and gpt-image-2 pricing', () => {
|
|
const marks = Array.from({ length: 7 }, (_, index) => ({
|
|
id: `mark-${index + 1}`,
|
|
tool: 'rect' as const,
|
|
x: index * 8,
|
|
y: index * 6,
|
|
width: 48,
|
|
height: 36,
|
|
}));
|
|
|
|
render(
|
|
<ImageCanvasUiAssetExtractionOverlayView
|
|
sourceLayer={createLayer({ src: 'data:image/png;base64,ui' })}
|
|
viewport={{ x: 10, y: 20, scale: 1 }}
|
|
state={createState({ marks })}
|
|
onToolChange={vi.fn()}
|
|
onPointerStart={vi.fn()}
|
|
onPointerMove={vi.fn()}
|
|
onPointerEnd={vi.fn()}
|
|
onSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole('toolbar', { name: 'UI素材框选工具' }).className,
|
|
).toContain('image-canvas-editor__ui-extraction-toolbar--right');
|
|
const panel = screen.getByRole('dialog', { name: 'UI素材提取' });
|
|
expect(panel.className).toContain(
|
|
'image-canvas-editor__ui-extraction-panel',
|
|
);
|
|
expect(
|
|
within(panel).getByText('使用框选工具框选你希望从画面中提取的素材'),
|
|
).toBeTruthy();
|
|
expect(within(panel).getAllByLabelText(/框选区域预览/u)).toHaveLength(7);
|
|
expect(within(panel).getByLabelText('计划规格').textContent).toBe('1:1·2K');
|
|
expect(within(panel).getByLabelText('固定模型').textContent).toContain('gpt-image-2');
|
|
expect(
|
|
within(panel).queryByRole('button', { name: '取消框选' }),
|
|
).toBeNull();
|
|
expect(within(panel).getByRole('button', { name: '提取' }).textContent).toContain('5泥点');
|
|
});
|
|
|
|
it('crops mark previews from the same source coordinates as the red selection', () => {
|
|
render(
|
|
<ImageCanvasUiAssetExtractionOverlayView
|
|
sourceLayer={createLayer({ src: 'data:image/png;base64,ui' })}
|
|
viewport={{ x: 10, y: 20, scale: 1 }}
|
|
state={createState({
|
|
marks: [
|
|
{
|
|
id: 'mark-1',
|
|
tool: 'rect',
|
|
x: 160,
|
|
y: 120,
|
|
width: 80,
|
|
height: 60,
|
|
},
|
|
],
|
|
})}
|
|
onToolChange={vi.fn()}
|
|
onPointerStart={vi.fn()}
|
|
onPointerMove={vi.fn()}
|
|
onPointerEnd={vi.fn()}
|
|
onSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const preview = screen.getByLabelText('框选区域预览 1');
|
|
const previewImage = within(preview).getByRole('img', {
|
|
name: '源图框选预览 1',
|
|
}) as HTMLImageElement;
|
|
|
|
expect(previewImage.getAttribute('src')).toBe('data:image/png;base64,ui');
|
|
expect(previewImage.style.width).toBe('800%');
|
|
expect(previewImage.style.height).toBe('800%');
|
|
expect(previewImage.style.left).toBe('-200%');
|
|
expect(previewImage.style.top).toBe('-200%');
|
|
});
|
|
|
|
it('reuses the source layer signed read-url cache for private crop previews', () => {
|
|
render(
|
|
<ImageCanvasUiAssetExtractionOverlayView
|
|
sourceLayer={createLayer({
|
|
src: '/generated-editor-assets/project-1/source.png',
|
|
objectKey: 'generated-editor-assets/project-1/source.png',
|
|
resourceId: 'resource-private',
|
|
taskId: 'task-private',
|
|
})}
|
|
viewport={{ x: 10, y: 20, scale: 1 }}
|
|
state={createState({
|
|
marks: [
|
|
{
|
|
id: 'mark-1',
|
|
tool: 'rect',
|
|
x: 160,
|
|
y: 120,
|
|
width: 80,
|
|
height: 60,
|
|
},
|
|
],
|
|
})}
|
|
onToolChange={vi.fn()}
|
|
onPointerStart={vi.fn()}
|
|
onPointerMove={vi.fn()}
|
|
onPointerEnd={vi.fn()}
|
|
onSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const preview = screen.getByLabelText('框选区域预览 1');
|
|
const previewImage = within(preview).getByRole('img', {
|
|
name: '源图框选预览 1',
|
|
});
|
|
|
|
expect(previewImage.getAttribute('data-object-key')).toBe(
|
|
'generated-editor-assets/project-1/source.png',
|
|
);
|
|
expect(previewImage.getAttribute('data-refresh-key')).toBe(
|
|
'task-private',
|
|
);
|
|
expect(previewImage.getAttribute('data-fallback-src')).toBe('');
|
|
});
|
|
|
|
it('shows numbered labels for quick edit red selections', () => {
|
|
const { container } = render(
|
|
<ImageCanvasUiAssetExtractionOverlayView
|
|
variant="quick-edit"
|
|
showIndexLabels
|
|
sourceLayer={createLayer()}
|
|
viewport={{ x: 0, y: 0, scale: 1 }}
|
|
state={createState({
|
|
marks: [
|
|
{
|
|
id: 'mark-1',
|
|
tool: 'rect',
|
|
x: 20,
|
|
y: 30,
|
|
width: 120,
|
|
height: 90,
|
|
},
|
|
{
|
|
id: 'mark-2',
|
|
tool: 'ellipse',
|
|
x: 220,
|
|
y: 120,
|
|
width: 80,
|
|
height: 60,
|
|
},
|
|
],
|
|
})}
|
|
onToolChange={vi.fn()}
|
|
onPointerStart={vi.fn()}
|
|
onPointerMove={vi.fn()}
|
|
onPointerEnd={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const labels = Array.from(
|
|
container.querySelectorAll(
|
|
'.image-canvas-editor__ui-extraction-mark-label',
|
|
),
|
|
).map((node) => node.textContent);
|
|
const labelCircles = Array.from(
|
|
container.querySelectorAll(
|
|
'.image-canvas-editor__ui-extraction-mark-label circle',
|
|
),
|
|
).map((node) => node.getAttribute('r'));
|
|
|
|
expect(labels).toEqual(['1', '2']);
|
|
expect(labelCircles).toEqual(['12', '12']);
|
|
});
|
|
});
|