f7126f9556
新增 HostBridge file.captureImage 契约与 H5 facade Expo 移动壳通过系统相机拍摄图片并复用图片导入校验 通用图片输入面板按宿主能力展示拍摄入口并转换为现有 File 回调 补充移动壳、HostBridge、图片面板测试和原生壳文档
1276 lines
45 KiB
TypeScript
1276 lines
45 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
canUseNativeHostCapability,
|
|
captureHostImageFile,
|
|
importHostImageFile,
|
|
subscribeHostImageDrop,
|
|
} from '../../services/host-bridge/hostBridge';
|
|
import { CreativeImageInputPanel } from './CreativeImageInputPanel';
|
|
|
|
vi.mock('../../services/host-bridge/hostBridge', () => ({
|
|
captureHostImageFile: vi.fn(),
|
|
canUseNativeHostCapability: vi.fn(() => false),
|
|
importHostImageFile: vi.fn(),
|
|
subscribeHostImageDrop: vi.fn(() => () => undefined),
|
|
}));
|
|
|
|
vi.mock('../ResolvedAssetImage', () => ({
|
|
ResolvedAssetImage: ({
|
|
src,
|
|
alt,
|
|
className,
|
|
}: {
|
|
src?: string | null;
|
|
alt?: string;
|
|
className?: string;
|
|
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
|
|
}));
|
|
|
|
const captureHostImageFileMock = vi.mocked(captureHostImageFile);
|
|
const canUseNativeHostCapabilityMock = vi.mocked(canUseNativeHostCapability);
|
|
const importHostImageFileMock = vi.mocked(importHostImageFile);
|
|
const subscribeHostImageDropMock = vi.mocked(subscribeHostImageDrop);
|
|
type HostImageDropListener = Parameters<typeof subscribeHostImageDrop>[0];
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
canUseNativeHostCapabilityMock.mockReturnValue(false);
|
|
subscribeHostImageDropMock.mockReturnValue(() => undefined);
|
|
});
|
|
|
|
function requireHostImageDropListener(
|
|
listener: HostImageDropListener | null,
|
|
) {
|
|
expect(listener).not.toBeNull();
|
|
return listener as HostImageDropListener;
|
|
}
|
|
|
|
test('creative image input panel handles reference uploads and preview', () => {
|
|
const onPromptReferenceFilesSelect = vi.fn();
|
|
const onPromptReferenceRemove = vi.fn();
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="image-upload-input"
|
|
promptTextareaId="image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[
|
|
{
|
|
id: 'ref-1',
|
|
label: '参考图 1',
|
|
imageSrc: 'data:image/png;base64,ref-1',
|
|
},
|
|
]}
|
|
imageModelPicker={<div />}
|
|
submitLabel="生成"
|
|
submitCostLabel="2泥点"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onPromptReferenceFilesSelect={onPromptReferenceFilesSelect}
|
|
onPromptReferenceRemove={onPromptReferenceRemove}
|
|
onSubmit={onSubmit}
|
|
/>,
|
|
);
|
|
|
|
const promptReferenceInput = screen.getByLabelText('上传参考图', {
|
|
selector: 'input',
|
|
});
|
|
const promptTextarea = screen.getByLabelText('画面描述');
|
|
const imageFieldLabel = screen.getByText('拼图画面');
|
|
const promptFieldLabel = screen.getByText('画面描述');
|
|
const emptyMainImageIconBadge = document.querySelector(
|
|
'[aria-hidden="true"]',
|
|
);
|
|
expect(imageFieldLabel.className).toContain('shrink-0');
|
|
expect(imageFieldLabel.className).toContain('text-sm font-black');
|
|
expect(promptFieldLabel.className).toContain('mb-0');
|
|
expect(promptFieldLabel.className).toContain('text-sm font-black');
|
|
expect(promptTextarea.className).toContain(
|
|
'border border-[var(--platform-subpanel-border)]',
|
|
);
|
|
expect(promptTextarea.className).toContain('rounded-[1.15rem]');
|
|
expect(promptTextarea.className).toContain('pb-14');
|
|
expect(emptyMainImageIconBadge?.className).toContain('h-14');
|
|
expect(emptyMainImageIconBadge?.className).toContain('rounded-full');
|
|
expect(emptyMainImageIconBadge?.className).toContain('bg-white/92');
|
|
expect((promptReferenceInput as HTMLInputElement).multiple).toBe(true);
|
|
|
|
fireEvent.change(promptReferenceInput, {
|
|
target: {
|
|
files: [
|
|
new File(['a'], 'ref-1.png', { type: 'image/png' }),
|
|
new File(['b'], 'ref-2.png', { type: 'image/png' }),
|
|
],
|
|
},
|
|
});
|
|
expect(onPromptReferenceFilesSelect).toHaveBeenCalledWith(
|
|
expect.arrayContaining([expect.any(File), expect.any(File)]),
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '预览参考图 参考图 1' }));
|
|
expect(screen.getByRole('dialog', { name: '参考图 1' })).toBeTruthy();
|
|
expect(screen.getByAltText('参考图预览')).toHaveProperty(
|
|
'src',
|
|
expect.stringContaining('ref-1'),
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭参考图预览' }));
|
|
expect(screen.queryByRole('dialog', { name: '参考图 1' })).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: '移除参考图 参考图 1' }));
|
|
expect(onPromptReferenceRemove).toHaveBeenCalledWith('ref-1');
|
|
|
|
const costBadge = screen.getByText('2泥点');
|
|
expect(costBadge.className).toContain('rounded-full');
|
|
expect(costBadge.className).toContain('bg-white/24');
|
|
expect(costBadge.className).toContain('text-current');
|
|
fireEvent.click(screen.getByRole('button', { name: /生成/u }));
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('creative image input panel can opt out of filling the parent height', () => {
|
|
const { container } = render(
|
|
<CreativeImageInputPanel
|
|
fillHeight={false}
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="image-upload-input"
|
|
promptTextareaId="image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const panel = container.querySelector('.creative-image-input-panel');
|
|
const body = container.querySelector('.creative-image-input-panel__body');
|
|
const section = container.querySelector(
|
|
'.creative-image-input-panel__section',
|
|
);
|
|
expect(panel?.className).toContain('flex-none');
|
|
expect(panel?.className).not.toContain('flex-1');
|
|
expect(body?.className).toContain('flex-none');
|
|
expect(body?.className).not.toContain('overflow-y-auto');
|
|
expect(section?.className).toContain('flex-none');
|
|
expect(section?.className).not.toContain('overflow-hidden');
|
|
});
|
|
|
|
test('creative image input panel fills the parent height by default', () => {
|
|
const { container } = render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="image-upload-input"
|
|
promptTextareaId="image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const panel = container.querySelector('.creative-image-input-panel');
|
|
const body = container.querySelector('.creative-image-input-panel__body');
|
|
const section = container.querySelector(
|
|
'.creative-image-input-panel__section',
|
|
);
|
|
expect(panel?.className).toContain('flex-1');
|
|
expect(panel?.className).not.toContain('flex-none');
|
|
expect(body?.className).toContain('flex-1');
|
|
expect(body?.className).toContain('overflow-y-auto');
|
|
expect(section?.className).toContain('flex-1');
|
|
expect(section?.className).toContain('overflow-hidden');
|
|
});
|
|
|
|
test('creative image input panel confirms before removing uploaded image', () => {
|
|
const onMainImageRemove = vi.fn();
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="data:image/png;base64,main"
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="image-upload-input"
|
|
promptTextareaId="image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面AI重绘要求(提示词)"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={<div />}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={onMainImageRemove}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '移除拼图图片' }));
|
|
const dialog = screen.getByRole('dialog', { name: '移除拼图图片?' });
|
|
expect(within(dialog).getByText('移除后需要重新上传图片。')).toBeTruthy();
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '移除' }));
|
|
expect(onMainImageRemove).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('creative image input panel closes reference preview with close button', () => {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="image-upload-input"
|
|
promptTextareaId="image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[
|
|
{
|
|
id: 'ref-1',
|
|
label: '参考图 1',
|
|
imageSrc: 'data:image/png;base64,ref-1',
|
|
},
|
|
]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onPromptReferenceFilesSelect={() => {}}
|
|
onPromptReferenceRemove={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '预览参考图 参考图 1' }));
|
|
expect(screen.getByRole('dialog', { name: '参考图 1' })).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭参考图预览' }));
|
|
expect(screen.queryByRole('dialog', { name: '参考图 1' })).toBeNull();
|
|
});
|
|
|
|
test('creative image input panel supports a preview-only main image mode', () => {
|
|
const onSubmit = vi.fn();
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
mainImageMode="preview"
|
|
uploadedImageSrc="/generated-puzzle-assets/session/ui/background.png"
|
|
uploadedImageAlt="UI背景预览"
|
|
mainImageInputId="background-image-upload-input"
|
|
promptTextareaId="background-prompt-input"
|
|
prompt="雨夜猫街竖屏拼图UI背景"
|
|
promptLabel="UI背景提示词"
|
|
aiRedraw={false}
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成UI背景"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: 'UI背景预览',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={onSubmit}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByAltText('UI背景预览').getAttribute('src')).toBe(
|
|
'/generated-puzzle-assets/session/ui/background.png',
|
|
);
|
|
expect(screen.queryByLabelText('上传拼图图片')).toBeNull();
|
|
expect(screen.queryByRole('button', { name: '选择历史图片' })).toBeNull();
|
|
expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull();
|
|
expect(screen.getByLabelText('UI背景提示词')).toHaveProperty(
|
|
'value',
|
|
'雨夜猫街竖屏拼图UI背景',
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '生成UI背景' }));
|
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('creative image input panel can preview the main image and keep upload on a corner button', () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
const inputClickSpy = vi
|
|
.spyOn(HTMLInputElement.prototype, 'click')
|
|
.mockImplementation(() => undefined);
|
|
|
|
try {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
mainImageClickMode="preview"
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
previewMainImage: '查看关卡图片',
|
|
closeMainImagePreview: '关闭关卡图片预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '查看关卡图片' }));
|
|
const previewDialog = screen.getByRole('dialog', {
|
|
name: '查看关卡图片',
|
|
});
|
|
expect(previewDialog.className).toContain('platform-image-preview-modal');
|
|
expect(previewDialog.className).toContain('bg-black');
|
|
expect(screen.getByRole('button', { name: '放大图片' })).toBeTruthy();
|
|
expect(screen.getAllByAltText('拼图关卡图').length).toBeGreaterThanOrEqual(
|
|
2,
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭关卡图片预览' }));
|
|
expect(
|
|
screen.queryByRole('dialog', { name: '查看关卡图片' }),
|
|
).toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '更换参考图' }));
|
|
expect(inputClickSpy).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.change(
|
|
screen.getByLabelText('上传参考图', { selector: 'input' }),
|
|
{
|
|
target: {
|
|
files: [
|
|
new File(['a'], 'level-reference.png', { type: 'image/png' }),
|
|
],
|
|
},
|
|
},
|
|
);
|
|
expect(onMainImageFileSelect).toHaveBeenCalledWith(expect.any(File));
|
|
} finally {
|
|
inputClickSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('creative image input panel closes main image preview with close button', () => {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
mainImageClickMode="preview"
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
previewMainImage: '查看关卡图片',
|
|
closeMainImagePreview: '关闭关卡图片预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '查看关卡图片' }));
|
|
expect(screen.getByRole('dialog', { name: '查看关卡图片' })).toBeTruthy();
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭关卡图片预览' }));
|
|
expect(screen.queryByRole('dialog', { name: '查看关卡图片' })).toBeNull();
|
|
});
|
|
|
|
test('creative image input panel can hide upload and history controls independently', () => {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
canUploadMainImage={false}
|
|
canUseImageHistory={false}
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
previewMainImage: '查看关卡图片',
|
|
closeMainImagePreview: '关闭关卡图片预览',
|
|
history: '选择历史图片',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onHistoryClick={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByRole('button', { name: '查看关卡图片' })).toBeTruthy();
|
|
expect(screen.queryByRole('button', { name: '更换参考图' })).toBeNull();
|
|
expect(
|
|
screen.queryByLabelText('上传参考图', { selector: 'input' }),
|
|
).toBeNull();
|
|
expect(screen.queryByRole('button', { name: '选择历史图片' })).toBeNull();
|
|
});
|
|
|
|
test('creative image input panel uses floating icon button for history action', () => {
|
|
const onHistoryClick = vi.fn();
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传描述参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
history: '选择历史图片',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onHistoryClick={onHistoryClick}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const historyButton = screen.getByRole('button', { name: '选择历史图片' });
|
|
|
|
expect(within(historyButton).getByText('历史')).toBeTruthy();
|
|
expect(historyButton.className).toContain('bg-white/94');
|
|
expect(historyButton.className).toContain('backdrop-blur');
|
|
expect(historyButton.className).toContain('gap-1.5');
|
|
|
|
fireEvent.click(historyButton);
|
|
expect(onHistoryClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('creative image input panel does not show empty upload hint over a non-removable image', () => {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
canRemoveMainImage={false}
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面AI重绘要求(提示词)"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByAltText('拼图关卡图')).toBeTruthy();
|
|
expect(screen.queryByText('上传图片/填写画面描述')).toBeNull();
|
|
expect(screen.queryByRole('button', { name: '移除参考图' })).toBeNull();
|
|
});
|
|
|
|
test('creative image input panel can show an image without exposing AI redraw controls', () => {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
canToggleAiRedraw={false}
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传参考图',
|
|
replaceImage: '更换参考图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除参考图',
|
|
removeImageConfirmTitle: '移除参考图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByAltText('拼图关卡图')).toBeTruthy();
|
|
expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull();
|
|
expect(screen.getByLabelText('画面描述')).toBeTruthy();
|
|
});
|
|
|
|
test('creative image input panel can upload prompt references while showing a main image', () => {
|
|
const onPromptReferenceFilesSelect = vi.fn();
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
|
|
uploadedImageAlt="拼图关卡图"
|
|
canUploadPromptReferences
|
|
mainImageInputId="level-image-upload-input"
|
|
promptTextareaId="level-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[
|
|
{
|
|
id: 'prompt-ref-1',
|
|
label: '描述参考图 1',
|
|
imageSrc: 'data:image/png;base64,prompt-ref-1',
|
|
},
|
|
]}
|
|
imageModelPicker={null}
|
|
submitLabel="重新生成画面"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '画面图',
|
|
uploadImage: '上传主图',
|
|
replaceImage: '更换主图',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除主图',
|
|
removeImageConfirmTitle: '移除主图?',
|
|
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onPromptReferenceFilesSelect={onPromptReferenceFilesSelect}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const promptReferenceInput = screen.getByLabelText('上传参考图', {
|
|
selector: 'input',
|
|
});
|
|
fireEvent.change(promptReferenceInput, {
|
|
target: {
|
|
files: [new File(['a'], 'prompt-reference.png', { type: 'image/png' })],
|
|
},
|
|
});
|
|
|
|
expect(onPromptReferenceFilesSelect).toHaveBeenCalledWith([expect.any(File)]);
|
|
expect(
|
|
screen.getByRole('button', { name: '预览参考图 描述参考图 1' }),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
test('creative image input panel imports the main image from native host capability', async () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
const inputClickSpy = vi
|
|
.spyOn(HTMLInputElement.prototype, 'click')
|
|
.mockImplementation(() => undefined);
|
|
canUseNativeHostCapabilityMock.mockReturnValue(true);
|
|
importHostImageFileMock.mockResolvedValue({
|
|
action: 'selected',
|
|
fileName: '参考图.png',
|
|
mimeType: 'image/png',
|
|
base64Data: 'aW1hZ2U=',
|
|
bytes: 5,
|
|
});
|
|
|
|
try {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="native-main-image-upload-input"
|
|
promptTextareaId="native-image-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '上传拼图图片' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onMainImageFileSelect).toHaveBeenCalledWith(expect.any(File));
|
|
});
|
|
const selectedFile = onMainImageFileSelect.mock.calls[0]?.[0] as File;
|
|
expect(selectedFile.name).toBe('参考图.png');
|
|
expect(selectedFile.type).toBe('image/png');
|
|
expect(inputClickSpy).not.toHaveBeenCalled();
|
|
} finally {
|
|
inputClickSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('creative image input panel imports prompt reference image from native host capability', async () => {
|
|
const onPromptReferenceFilesSelect = vi.fn();
|
|
canUseNativeHostCapabilityMock.mockReturnValue(true);
|
|
importHostImageFileMock.mockResolvedValue({
|
|
action: 'selected',
|
|
fileName: '描述参考.webp',
|
|
mimeType: 'image/webp',
|
|
base64Data: 'cmVm',
|
|
bytes: 3,
|
|
});
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="native-reference-main-image-upload-input"
|
|
promptTextareaId="native-reference-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onPromptReferenceFilesSelect={onPromptReferenceFilesSelect}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '上传参考图' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onPromptReferenceFilesSelect).toHaveBeenCalledWith([
|
|
expect.any(File),
|
|
]);
|
|
});
|
|
const selectedFile = onPromptReferenceFilesSelect.mock.calls[0]?.[0]?.[0] as
|
|
| File
|
|
| undefined;
|
|
expect(selectedFile?.name).toBe('描述参考.webp');
|
|
expect(selectedFile?.type).toBe('image/webp');
|
|
});
|
|
|
|
test('creative image input panel captures the main image from native host capability', async () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
const inputClickSpy = vi
|
|
.spyOn(HTMLInputElement.prototype, 'click')
|
|
.mockImplementation(() => undefined);
|
|
canUseNativeHostCapabilityMock.mockImplementation(
|
|
(capability) => capability === 'file.captureImage',
|
|
);
|
|
captureHostImageFileMock.mockResolvedValue({
|
|
action: 'captured',
|
|
fileName: '拍摄图.jpg',
|
|
mimeType: 'image/jpeg',
|
|
base64Data: 'Y2FtZXJh',
|
|
bytes: 6,
|
|
});
|
|
|
|
try {
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="native-capture-main-image-input"
|
|
promptTextareaId="native-capture-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拍摄图片' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onMainImageFileSelect).toHaveBeenCalledWith(expect.any(File));
|
|
});
|
|
const selectedFile = onMainImageFileSelect.mock.calls[0]?.[0] as File;
|
|
expect(selectedFile.name).toBe('拍摄图.jpg');
|
|
expect(selectedFile.type).toBe('image/jpeg');
|
|
expect(inputClickSpy).not.toHaveBeenCalled();
|
|
expect(importHostImageFileMock).not.toHaveBeenCalled();
|
|
} finally {
|
|
inputClickSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
test('creative image input panel captures prompt reference image from native host capability', async () => {
|
|
const onPromptReferenceFilesSelect = vi.fn();
|
|
canUseNativeHostCapabilityMock.mockImplementation(
|
|
(capability) => capability === 'file.captureImage',
|
|
);
|
|
captureHostImageFileMock.mockResolvedValue({
|
|
action: 'captured',
|
|
fileName: '参考拍摄.png',
|
|
mimeType: 'image/png',
|
|
base64Data: 'cmVm',
|
|
bytes: 3,
|
|
});
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="native-capture-reference-main-image-input"
|
|
promptTextareaId="native-capture-reference-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={() => {}}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onPromptReferenceFilesSelect={onPromptReferenceFilesSelect}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const captureButtons = screen.getAllByRole('button', { name: '拍摄图片' });
|
|
expect(captureButtons).toHaveLength(2);
|
|
fireEvent.click(captureButtons[1] as HTMLElement);
|
|
|
|
await waitFor(() => {
|
|
expect(onPromptReferenceFilesSelect).toHaveBeenCalledWith([
|
|
expect.any(File),
|
|
]);
|
|
});
|
|
const selectedFile = onPromptReferenceFilesSelect.mock.calls[0]?.[0]?.[0] as
|
|
| File
|
|
| undefined;
|
|
expect(selectedFile?.name).toBe('参考拍摄.png');
|
|
expect(selectedFile?.type).toBe('image/png');
|
|
expect(importHostImageFileMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('creative image input panel keeps callbacks quiet when native image capture is cancelled', async () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
canUseNativeHostCapabilityMock.mockImplementation(
|
|
(capability) => capability === 'file.captureImage',
|
|
);
|
|
captureHostImageFileMock.mockResolvedValue(false);
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="cancelled-native-capture-image-input"
|
|
promptTextareaId="cancelled-native-capture-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '拍摄图片' }));
|
|
|
|
await waitFor(() => {
|
|
expect(captureHostImageFileMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(onMainImageFileSelect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('creative image input panel keeps callbacks quiet when native image import is cancelled', async () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
canUseNativeHostCapabilityMock.mockReturnValue(true);
|
|
importHostImageFileMock.mockResolvedValue(false);
|
|
|
|
render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="cancelled-native-image-upload-input"
|
|
promptTextareaId="cancelled-native-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '上传拼图图片' }));
|
|
|
|
await waitFor(() => {
|
|
expect(importHostImageFileMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(onMainImageFileSelect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('creative image input panel accepts desktop host image drops inside the main image card', async () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
let dropListener: HostImageDropListener | null = null;
|
|
canUseNativeHostCapabilityMock.mockImplementation(
|
|
(capability) =>
|
|
capability === 'file.importImage' || capability === 'file.imageDropped',
|
|
);
|
|
subscribeHostImageDropMock.mockImplementation((listener) => {
|
|
dropListener = listener;
|
|
return () => undefined;
|
|
});
|
|
|
|
const { container } = render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="dropped-main-image-upload-input"
|
|
promptTextareaId="dropped-native-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const card = container.querySelector('.creative-image-input-panel__image-card');
|
|
expect(card).not.toBeNull();
|
|
vi.spyOn(card as Element, 'getBoundingClientRect').mockReturnValue({
|
|
left: 10,
|
|
top: 20,
|
|
right: 210,
|
|
bottom: 220,
|
|
width: 200,
|
|
height: 200,
|
|
x: 10,
|
|
y: 20,
|
|
toJSON: () => ({}),
|
|
});
|
|
const elementFromPointMock = vi.fn(() => card);
|
|
Object.defineProperty(document, 'elementFromPoint', {
|
|
configurable: true,
|
|
value: elementFromPointMock,
|
|
});
|
|
|
|
requireHostImageDropListener(dropListener)({
|
|
action: 'dropped',
|
|
fileName: '拖入图.webp',
|
|
mimeType: 'image/webp',
|
|
base64Data: 'ZHJvcA==',
|
|
bytes: 4,
|
|
position: {
|
|
x: 32,
|
|
y: 48,
|
|
},
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(onMainImageFileSelect).toHaveBeenCalledWith(expect.any(File));
|
|
});
|
|
const selectedFile = onMainImageFileSelect.mock.calls[0]?.[0] as File;
|
|
expect(selectedFile.name).toBe('拖入图.webp');
|
|
expect(selectedFile.type).toBe('image/webp');
|
|
Reflect.deleteProperty(document, 'elementFromPoint');
|
|
});
|
|
|
|
test('creative image input panel ignores desktop host image drops outside or behind another element', () => {
|
|
const onMainImageFileSelect = vi.fn();
|
|
let dropListener: HostImageDropListener | null = null;
|
|
canUseNativeHostCapabilityMock.mockImplementation(
|
|
(capability) =>
|
|
capability === 'file.importImage' || capability === 'file.imageDropped',
|
|
);
|
|
subscribeHostImageDropMock.mockImplementation((listener) => {
|
|
dropListener = listener;
|
|
return () => undefined;
|
|
});
|
|
|
|
const { container } = render(
|
|
<CreativeImageInputPanel
|
|
uploadedImageSrc=""
|
|
uploadedImageAlt="拼图图片"
|
|
mainImageInputId="ignored-drop-main-image-upload-input"
|
|
promptTextareaId="ignored-drop-prompt-input"
|
|
prompt="旧街灯牌下的猫。"
|
|
promptLabel="画面描述"
|
|
aiRedraw
|
|
promptReferenceImages={[]}
|
|
imageModelPicker={null}
|
|
submitLabel="生成"
|
|
submitDisabled={false}
|
|
labels={{
|
|
imageField: '拼图画面',
|
|
uploadImage: '上传拼图图片',
|
|
replaceImage: '更换拼图图片',
|
|
emptyImageHint: '上传图片/填写画面描述',
|
|
removeImage: '移除拼图图片',
|
|
removeImageConfirmTitle: '移除拼图图片?',
|
|
removeImageConfirmBody: '移除后需要重新上传图片。',
|
|
promptReferenceUpload: '上传参考图',
|
|
promptReferencePreviewAlt: '参考图预览',
|
|
closePromptReferencePreview: '关闭参考图预览',
|
|
}}
|
|
onMainImageFileSelect={onMainImageFileSelect}
|
|
onMainImageRemove={() => {}}
|
|
onAiRedrawChange={() => {}}
|
|
onPromptChange={() => {}}
|
|
onSubmit={() => {}}
|
|
/>,
|
|
);
|
|
|
|
const card = container.querySelector('.creative-image-input-panel__image-card');
|
|
expect(card).not.toBeNull();
|
|
vi.spyOn(card as Element, 'getBoundingClientRect').mockReturnValue({
|
|
left: 10,
|
|
top: 20,
|
|
right: 210,
|
|
bottom: 220,
|
|
width: 200,
|
|
height: 200,
|
|
x: 10,
|
|
y: 20,
|
|
toJSON: () => ({}),
|
|
});
|
|
|
|
const emitDrop = requireHostImageDropListener(dropListener);
|
|
|
|
emitDrop({
|
|
action: 'dropped',
|
|
fileName: '卡片外.png',
|
|
mimeType: 'image/png',
|
|
base64Data: 'b3V0',
|
|
bytes: 3,
|
|
position: {
|
|
x: 4,
|
|
y: 48,
|
|
},
|
|
});
|
|
expect(onMainImageFileSelect).not.toHaveBeenCalled();
|
|
|
|
const overlay = document.createElement('div');
|
|
const elementFromPointMock = vi.fn(() => overlay);
|
|
Object.defineProperty(document, 'elementFromPoint', {
|
|
configurable: true,
|
|
value: elementFromPointMock,
|
|
});
|
|
emitDrop({
|
|
action: 'dropped',
|
|
fileName: '遮挡图.png',
|
|
mimeType: 'image/png',
|
|
base64Data: 'b3Zlcg==',
|
|
bytes: 4,
|
|
position: {
|
|
x: 32,
|
|
y: 48,
|
|
},
|
|
});
|
|
expect(onMainImageFileSelect).not.toHaveBeenCalled();
|
|
Reflect.deleteProperty(document, 'elementFromPoint');
|
|
});
|