7edc452c6a
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
主菜单保留前五项,其余操作通过向上展开的更多菜单访问 类型与信息入口下沉资源卡片,并复用共享画布角标 补充菜单键盘、滚轮归属、换选及资源操作链路回归 同步方案A规范与待人工验收记录
792 lines
26 KiB
TypeScript
792 lines
26 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,
|
|
type ImageCanvasSelectedToolbarAction,
|
|
} 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: 'generated',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'image.generate',
|
|
fields: [{ id: 'prompt', title: '生成提示词', value: '测试图片' }],
|
|
references: [],
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function renderSelectedToolbar(
|
|
overrides: Partial<
|
|
Parameters<typeof ImageCanvasSelectedLayerToolbarView>[0]
|
|
> = {},
|
|
) {
|
|
const selectedLayer = createLayer();
|
|
const props: Parameters<typeof ImageCanvasSelectedLayerToolbarView>[0] = {
|
|
selectedLayer,
|
|
selectedToolbarStyle: { left: 12, top: 24 },
|
|
onOpenQuickEditPanel: vi.fn(),
|
|
onOpenRedrawPanel: vi.fn(),
|
|
onOpenCropExpandPanel: vi.fn(),
|
|
onRemoveBackground: vi.fn(),
|
|
onPerfectPixel: vi.fn(),
|
|
isPerfectPixelProcessing: false,
|
|
isPerfectPixelPendingConfirmation: false,
|
|
isSplittingIconSpritesheet: false,
|
|
isPersistingAssetKind: false,
|
|
onSplitIconSpritesheet: vi.fn(),
|
|
onExtractUiDesignAssets: vi.fn(),
|
|
onOpenCharacterAnimationPanel: vi.fn(),
|
|
onDownloadLayer: vi.fn(),
|
|
...overrides,
|
|
};
|
|
|
|
render(<ImageCanvasSelectedLayerToolbarView {...props} />);
|
|
|
|
return props;
|
|
}
|
|
|
|
describe('ImageCanvasSelectedLayerToolbarView', () => {
|
|
it('AGC 收纳包含宿主末组,换选后关闭旧资源更多菜单', () => {
|
|
const props = renderSelectedToolbar({
|
|
maxVisibleActions: 5,
|
|
supportedActions: new Set([
|
|
'quick-edit',
|
|
'character-animation',
|
|
'download',
|
|
]),
|
|
downloadLabel: '导出',
|
|
extraActions: (
|
|
<>
|
|
<button>引用</button>
|
|
<button>编辑标签</button>
|
|
<button>重命名</button>
|
|
</>
|
|
),
|
|
endActions: <button>删除素材</button>,
|
|
});
|
|
const toolbar = screen.getByRole('toolbar');
|
|
expect(
|
|
within(toolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.textContent),
|
|
).toEqual(['快速编辑', '生成动画', '引用', '编辑标签', '重命名', '更多 ▴']);
|
|
fireEvent.mouseEnter(screen.getByRole('button', { name: '更多' }));
|
|
const group = screen.getByRole('group', { name: '更多操作' });
|
|
expect(
|
|
within(group)
|
|
.getAllByRole('button')
|
|
.map((button) => button.textContent),
|
|
).toEqual(['导出', '删除素材']);
|
|
fireEvent.click(within(group).getByRole('button', { name: '导出' }));
|
|
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
|
cleanup();
|
|
const view = render(<ImageCanvasSelectedLayerToolbarView {...props} />);
|
|
fireEvent.mouseEnter(screen.getByRole('button', { name: '更多' }));
|
|
view.rerender(
|
|
<ImageCanvasSelectedLayerToolbarView
|
|
{...props}
|
|
selectedLayer={createLayer({ id: 'other' })}
|
|
/>,
|
|
);
|
|
expect(screen.queryByRole('group', { name: '更多操作' })).toBeNull();
|
|
});
|
|
it('常规动作与末组分别分隔,导出紧邻删除且位于删除之前', () => {
|
|
const props = renderSelectedToolbar({
|
|
supportedActions: new Set(['quick-edit', 'download']),
|
|
downloadLabel: '导出',
|
|
extraActions: <button>引用</button>,
|
|
endActions: <button>删除素材</button>,
|
|
});
|
|
const toolbar = screen.getByRole('toolbar');
|
|
expect(
|
|
toolbar.querySelectorAll(
|
|
'.image-canvas-editor__floating-toolbar-divider',
|
|
),
|
|
).toHaveLength(2);
|
|
expect(
|
|
within(toolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.textContent),
|
|
).toEqual(['快速编辑', '引用', '导出', '删除素材']);
|
|
expect(
|
|
screen.getByRole('button', { name: '导出' }).nextElementSibling,
|
|
).toBe(screen.getByRole('button', { name: '删除素材' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '导出' }));
|
|
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
|
});
|
|
|
|
it('任意动作组合都不生成相邻、开头或结尾分隔线', () => {
|
|
const actions: ImageCanvasSelectedToolbarAction[] = [
|
|
'quick-edit',
|
|
'crop-expand',
|
|
'redraw',
|
|
'download',
|
|
];
|
|
for (const mediaType of ['image', 'audio', 'video'] as const) {
|
|
for (let mask = 0; mask < 1 << actions.length; mask += 1) {
|
|
for (const withExtraActions of [false, true]) {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({ mediaType }),
|
|
supportedActions: new Set(
|
|
actions.filter((_, index) => (mask & (1 << index)) !== 0),
|
|
),
|
|
extraActions: withExtraActions ? <button>信息</button> : null,
|
|
});
|
|
const toolbar = screen.getByRole('toolbar');
|
|
const divider = '.image-canvas-editor__floating-toolbar-divider';
|
|
expect(toolbar.querySelector(`${divider} + ${divider}`)).toBeNull();
|
|
expect(toolbar.firstElementChild?.matches(divider) ?? false).toBe(
|
|
false,
|
|
);
|
|
expect(toolbar.lastElementChild?.matches(divider) ?? false).toBe(
|
|
false,
|
|
);
|
|
cleanup();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('空的条件动作和嵌套 Fragment 不产生空分组', () => {
|
|
renderSelectedToolbar({
|
|
supportedActions: new Set(['quick-edit', 'download']),
|
|
extraActions: (
|
|
<>
|
|
{null}
|
|
<>{false}</>
|
|
</>
|
|
),
|
|
endActions: <>{null}</>,
|
|
});
|
|
const toolbar = screen.getByRole('toolbar');
|
|
expect(
|
|
toolbar.querySelectorAll(
|
|
'.image-canvas-editor__floating-toolbar-divider',
|
|
),
|
|
).toHaveLength(1);
|
|
expect(
|
|
within(toolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.getAttribute('aria-label')),
|
|
).toEqual(['快速编辑', '下载按钮']);
|
|
expect(toolbar.lastElementChild?.tagName).toBe('BUTTON');
|
|
});
|
|
|
|
it.each(['image', 'audio', 'video', 'image-sequence', undefined] as const)(
|
|
'%s 素材使用同一个带文字导出入口与回调',
|
|
(mediaType) => {
|
|
const props = renderSelectedToolbar({
|
|
selectedLayer: createLayer({ mediaType }),
|
|
supportedActions: new Set(['download']),
|
|
downloadLabel: '导出',
|
|
extraActions: <button>信息</button>,
|
|
endActions: <button>删除素材</button>,
|
|
});
|
|
const toolbar = screen.getByRole('toolbar');
|
|
expect(
|
|
within(toolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.textContent),
|
|
).toEqual(['信息', '导出', '删除素材']);
|
|
expect(
|
|
screen.getByRole('button', { name: '导出' }).nextElementSibling,
|
|
).toBe(screen.getByRole('button', { name: '删除素材' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '导出' }));
|
|
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
|
},
|
|
);
|
|
|
|
it('宿主未放行下载时不展示导出入口', () => {
|
|
renderSelectedToolbar({
|
|
supportedActions: new Set(),
|
|
downloadLabel: '导出',
|
|
extraActions: <button>信息</button>,
|
|
});
|
|
expect(screen.queryByRole('button', { name: '导出' })).toBeNull();
|
|
});
|
|
|
|
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: '改造' }));
|
|
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.onPerfectPixel).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();
|
|
expect(
|
|
within(toolbar)
|
|
.getByRole('button', { name: '去除背景按钮' })
|
|
.querySelector('.lucide-image-off'),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(toolbar)
|
|
.getByRole('button', { name: '完美像素' })
|
|
.querySelector('.lucide-grid-2x2'),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(toolbar).getByRole('button', { name: '完美像素' }).textContent,
|
|
).toContain('完美像素');
|
|
expect(
|
|
within(toolbar)
|
|
.getByRole('button', { name: '完美像素' })
|
|
.getAttribute('title'),
|
|
).toBe('自动识别并规整像素网格');
|
|
});
|
|
|
|
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 icon spritesheet splitting after remove background', () => {
|
|
const layer = createLayer({
|
|
sourceType: 'generated',
|
|
assetKind: 'icon-spritesheet',
|
|
});
|
|
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([
|
|
'快速编辑',
|
|
'裁扩按钮',
|
|
'去除背景按钮',
|
|
'完美像素',
|
|
'拆分图集',
|
|
'改造',
|
|
'下载按钮',
|
|
]);
|
|
|
|
const splitButton = within(toolbar).getByRole('button', {
|
|
name: '拆分图集',
|
|
});
|
|
expect(splitButton.querySelector('.lucide-scissors')).toBeTruthy();
|
|
|
|
fireEvent.click(splitButton);
|
|
|
|
expect(props.onSplitIconSpritesheet).toHaveBeenCalledWith(layer);
|
|
});
|
|
|
|
it('renders a disabled loading state while splitting an icon spritesheet', () => {
|
|
const layer = createLayer({
|
|
sourceType: 'generated',
|
|
assetKind: 'icon-spritesheet',
|
|
});
|
|
const props = renderSelectedToolbar({
|
|
selectedLayer: layer,
|
|
isSplittingIconSpritesheet: true,
|
|
});
|
|
const splitButton = screen.getByRole('button', { name: '拆图中' });
|
|
|
|
expect(splitButton.getAttribute('title')).toBe('拆图中');
|
|
expect(splitButton.getAttribute('aria-busy')).toBe('true');
|
|
expect((splitButton as HTMLButtonElement).disabled).toBe(true);
|
|
expect(splitButton.textContent).toContain('拆图中');
|
|
expect(splitButton.querySelector('.lucide-loader-circle')).toBeTruthy();
|
|
expect(
|
|
splitButton.querySelector('.lucide-loader-circle.animate-spin'),
|
|
).toBeTruthy();
|
|
expect(splitButton.querySelector('.lucide-scissors')).toBeNull();
|
|
|
|
fireEvent.click(splitButton);
|
|
fireEvent.click(splitButton);
|
|
|
|
expect(props.onSplitIconSpritesheet).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders a disabled loading state while perfect pixel is processing', () => {
|
|
const props = renderSelectedToolbar({
|
|
isPerfectPixelProcessing: true,
|
|
});
|
|
const button = screen.getByRole('button', {
|
|
name: '完美像素处理中',
|
|
});
|
|
|
|
expect(button.getAttribute('aria-busy')).toBe('true');
|
|
expect((button as HTMLButtonElement).disabled).toBe(true);
|
|
expect(
|
|
button.querySelector('.lucide-loader-circle.animate-spin'),
|
|
).toBeTruthy();
|
|
expect(button.querySelector('.lucide-grid-2x2')).toBeNull();
|
|
expect(button.textContent).toContain('处理中');
|
|
|
|
fireEvent.click(button);
|
|
fireEvent.click(button);
|
|
|
|
expect(props.onPerfectPixel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders a distinct disabled state while perfect pixel is pending confirmation', () => {
|
|
const props = renderSelectedToolbar({
|
|
isPerfectPixelPendingConfirmation: true,
|
|
});
|
|
const button = screen.getByRole('button', {
|
|
name: '完美像素结果待确认',
|
|
});
|
|
|
|
expect(button.getAttribute('title')).toBe(
|
|
'结果待确认,双击原占位继续核对或重试',
|
|
);
|
|
expect(button.getAttribute('aria-busy')).toBe('false');
|
|
expect((button as HTMLButtonElement).disabled).toBe(true);
|
|
expect(button.textContent).toContain('待确认');
|
|
expect(button.textContent).not.toContain('处理中');
|
|
expect(button.querySelector('.lucide-grid-2x2')).toBeTruthy();
|
|
expect(button.querySelector('.lucide-loader-circle')).toBeNull();
|
|
|
|
fireEvent.click(button);
|
|
|
|
expect(props.onPerfectPixel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not invoke perfect pixel while the selected asset kind is persisting', () => {
|
|
// 中文注释:请求同时带 assetKind 与 sourceResourceId,本地类型已改但资源尚未落库时
|
|
// 两者不一致,后端会直接 400 并留下失败占位。名称与拆分图集按钮的「素材类型保存中」
|
|
// 必须区分开——icon-spritesheet 图层上两个按钮会同时进入保存态。
|
|
const layer = createLayer({ assetKind: 'icon-spritesheet' });
|
|
const props = renderSelectedToolbar({
|
|
selectedLayer: layer,
|
|
isPersistingAssetKind: true,
|
|
});
|
|
const button = screen.getByRole('button', {
|
|
name: '完美像素等待素材类型保存',
|
|
});
|
|
|
|
expect(button.getAttribute('aria-busy')).toBe('true');
|
|
expect((button as HTMLButtonElement).disabled).toBe(true);
|
|
expect(button.querySelector('.lucide-grid-2x2')).toBeNull();
|
|
expect(button.textContent).toContain('保存中');
|
|
|
|
fireEvent.click(button);
|
|
|
|
expect(props.onPerfectPixel).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not invoke atlas splitting while the selected asset kind is persisting', () => {
|
|
const layer = createLayer({ assetKind: 'icon-spritesheet' });
|
|
const props = renderSelectedToolbar({
|
|
selectedLayer: layer,
|
|
isPersistingAssetKind: true,
|
|
});
|
|
const splitButton = screen.getByRole('button', {
|
|
name: '素材类型保存中',
|
|
});
|
|
|
|
expect(splitButton.getAttribute('aria-busy')).toBe('true');
|
|
expect((splitButton as HTMLButtonElement).disabled).toBe(true);
|
|
expect(splitButton.textContent).toContain('保存中');
|
|
|
|
fireEvent.click(splitButton);
|
|
|
|
expect(props.onSplitIconSpritesheet).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps only remodel and download for audio layers', () => {
|
|
const layer = createLayer({
|
|
title: '游戏音效',
|
|
src: 'data:audio/mpeg;base64,audio',
|
|
mediaType: 'audio',
|
|
assetKind: 'sound-effect',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'audio.sound-effect.generate',
|
|
fields: [{ id: 'prompt', title: 'prompt', value: '金币声' }],
|
|
references: [],
|
|
},
|
|
});
|
|
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([
|
|
'改造',
|
|
'下载按钮',
|
|
]);
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '快速编辑' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '裁扩按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '去除背景按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '完美像素' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(toolbar).queryByRole('button', { name: '生成动画' }),
|
|
).toBeNull();
|
|
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '改造' }));
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '下载按钮' }));
|
|
|
|
expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(layer);
|
|
expect(props.onDownloadLayer).toHaveBeenCalledWith(layer);
|
|
});
|
|
|
|
it('hides redraw for non-generative derived and public display-only layers', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
title: '手动裁扩结果',
|
|
generationInputs: null,
|
|
}),
|
|
});
|
|
expect(screen.queryByRole('button', { name: '改造' })).toBeNull();
|
|
|
|
cleanup();
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
title: '公开音效',
|
|
mediaType: 'audio',
|
|
assetKind: 'sound-effect',
|
|
generationInputs: {
|
|
version: 2,
|
|
fields: [{ title: 'prompt', value: '公开展示文本' }],
|
|
references: [],
|
|
},
|
|
}),
|
|
});
|
|
expect(screen.queryByRole('button', { name: '改造' })).toBeNull();
|
|
expect(screen.getByRole('button', { name: '下载按钮' })).toBeTruthy();
|
|
});
|
|
|
|
it.each([
|
|
'image.perfect-pixel',
|
|
'spritesheet.split',
|
|
'image.remove-background',
|
|
'image.crop-expand',
|
|
] as const)('never exposes redraw for deterministic action %s', (action) => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
generationInputs: {
|
|
version: 2,
|
|
action,
|
|
fields: [],
|
|
references: [],
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.queryByRole('button', { name: '改造' })).toBeNull();
|
|
});
|
|
|
|
it('keeps quick edit but hides redraw for in-place quick-edit results', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'image.edit',
|
|
fields: [
|
|
{ id: 'prompt', title: '快速编辑提示词', value: '改成夜景' },
|
|
],
|
|
references: [
|
|
{
|
|
id: 'source',
|
|
title: '原图',
|
|
refType: 'project-resource',
|
|
refId: 'resource-before-in-place-edit',
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: '快速编辑' })).toBeTruthy();
|
|
expect(screen.queryByRole('button', { name: '改造' })).toBeNull();
|
|
});
|
|
|
|
it('removes raster edit actions for video and character animation layers', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
title: '生成视频',
|
|
src: '/generated/video.mp4',
|
|
mediaType: 'video',
|
|
assetKind: 'video',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'video.generate',
|
|
fields: [{ id: 'prompt', title: '视频描述', value: '测试视频' }],
|
|
references: [],
|
|
},
|
|
}),
|
|
});
|
|
const videoToolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
|
|
|
|
expect(
|
|
within(videoToolbar).queryByRole('button', { name: '裁扩按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(videoToolbar).queryByRole('button', { name: '去除背景按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(videoToolbar).queryByRole('button', { name: '完美像素' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(videoToolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.getAttribute('aria-label')),
|
|
).toEqual(['改造', '下载按钮']);
|
|
|
|
cleanup();
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({
|
|
title: '角色动作',
|
|
src: '/generated/action/frame-01.png',
|
|
mediaType: 'image-sequence',
|
|
assetKind: 'character-animation',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'character-animation.generate',
|
|
fields: [{ id: 'prompt', title: '动作描述', value: '挥手' }],
|
|
references: [],
|
|
},
|
|
}),
|
|
});
|
|
const actionToolbar = screen.getByRole('toolbar', { name: '图片工具栏' });
|
|
|
|
expect(
|
|
within(actionToolbar).queryByRole('button', { name: '裁扩按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(actionToolbar).queryByRole('button', { name: '去除背景按钮' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(actionToolbar).queryByRole('button', { name: '完美像素' }),
|
|
).toBeNull();
|
|
expect(
|
|
within(actionToolbar)
|
|
.getAllByRole('button')
|
|
.map((button) => button.getAttribute('aria-label')),
|
|
).toEqual(['改造', '下载按钮']);
|
|
});
|
|
|
|
it('keeps redraw visible for supported actions with a missing non-replaceable source', () => {
|
|
const generatedLayer = createLayer({
|
|
id: 'animation-layer',
|
|
resourceId: 'animation-resource',
|
|
mediaType: 'image-sequence',
|
|
assetKind: 'character-animation',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'character-animation.generate',
|
|
fields: [{ id: 'prompt', title: '动作描述', value: '挥手' }],
|
|
references: [
|
|
{
|
|
id: 'source',
|
|
title: '原角色图',
|
|
label: '角色',
|
|
refType: 'project-resource',
|
|
refId: 'missing-source-resource',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const props = renderSelectedToolbar({ selectedLayer: generatedLayer });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '改造' }));
|
|
|
|
expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(generatedLayer);
|
|
|
|
cleanup();
|
|
const uiDesignLayer = createLayer({
|
|
id: 'ui-spritesheet-layer',
|
|
resourceId: 'ui-spritesheet-resource',
|
|
assetKind: 'icon-spritesheet',
|
|
generationInputs: {
|
|
version: 2,
|
|
action: 'ui-design.extract-assets',
|
|
fields: [{ id: 'prompt', title: '提取提示词', value: '提取按钮' }],
|
|
references: [
|
|
{
|
|
id: 'source',
|
|
title: 'UI设计图',
|
|
refType: 'project-resource',
|
|
refId: 'missing-ui-design-resource',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const uiProps = renderSelectedToolbar({ selectedLayer: uiDesignLayer });
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '改造' }));
|
|
|
|
expect(uiProps.onOpenRedrawPanel).toHaveBeenCalledWith(uiDesignLayer);
|
|
});
|
|
|
|
it('renders character animation only for character layers', () => {
|
|
const layer = createLayer({
|
|
sourceType: 'generated',
|
|
assetKind: 'character',
|
|
});
|
|
const props = renderSelectedToolbar({ selectedLayer: layer });
|
|
const animationButton = screen.getByRole('button', { name: '生成动画' });
|
|
|
|
expect(animationButton.textContent).toContain('生成动画');
|
|
expect(
|
|
animationButton.querySelector('.lucide-person-standing'),
|
|
).toBeTruthy();
|
|
|
|
fireEvent.click(animationButton);
|
|
|
|
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
|
|
|
|
cleanup();
|
|
renderSelectedToolbar({ selectedLayer: createLayer({ id: 'layer-2' }) });
|
|
expect(screen.queryByRole('button', { name: '生成动画' })).toBeNull();
|
|
});
|
|
|
|
it('hides quick edit only for individual icon assets', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({ assetKind: 'icon' }),
|
|
});
|
|
|
|
expect(screen.queryByRole('button', { name: '快速编辑' })).toBeNull();
|
|
});
|
|
|
|
it('keeps quick edit and manual split available for icon spritesheets', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({ assetKind: 'icon-spritesheet' }),
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: '快速编辑' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '拆分图集' })).toBeTruthy();
|
|
});
|
|
|
|
it('keeps quick edit available for icon specs', () => {
|
|
renderSelectedToolbar({
|
|
selectedLayer: createLayer({ assetKind: 'icon-spec' }),
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: '快速编辑' })).toBeTruthy();
|
|
});
|
|
|
|
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()}
|
|
onPerfectPixel={vi.fn()}
|
|
isPerfectPixelProcessing={false}
|
|
isPerfectPixelPendingConfirmation={false}
|
|
isSplittingIconSpritesheet={false}
|
|
isPersistingAssetKind={false}
|
|
onSplitIconSpritesheet={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()}
|
|
onPerfectPixel={vi.fn()}
|
|
isPerfectPixelProcessing={false}
|
|
isPerfectPixelPendingConfirmation={false}
|
|
isSplittingIconSpritesheet={false}
|
|
isPersistingAssetKind={false}
|
|
onSplitIconSpritesheet={vi.fn()}
|
|
onExtractUiDesignAssets={vi.fn()}
|
|
onOpenCharacterAnimationPanel={vi.fn()}
|
|
onDownloadLayer={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByRole('toolbar', { name: '图片工具栏' })).toBeNull();
|
|
});
|
|
});
|