Files
Genarrative/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx
T
JenkenB 7830d201c4 Server-side billing for editor generation
Move editor generation pricing to backend: remove priceMudPoints from public request schemas and docs, require backend to compute model prices and perform pre-charge via execute_billable_asset_operation_with_cost. Update character animation and video handlers to use authenticated owner IDs and wrap generation requests in billing; remove client-side price validation. Adjust OpenAPI, project-memory docs, pricing management docs, and related tests to reflect that front-end only displays prices and must not submit priceMudPoints. Also document new user registration reward and wallet refund outbox behavior. Code and test changes applied across api-server handlers, shared contracts, and image-editor client/tests.
2026-06-22 23:11:23 +08:00

214 lines
7.2 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { createRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import type {
GenerateDialogState,
UploadTarget,
} from './ImageCanvasEditorTypes';
function createDialog(
patch: Partial<GenerateDialogState> = {},
): GenerateDialogState {
return {
mode: 'generate',
prompt: '初始提示',
status: 'idle',
generationReferences: [],
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
...patch,
};
}
function BasicGenerationHarness({
initialDialog = createDialog(),
onRequestUpload = vi.fn(),
onSubmit = vi.fn(),
}: {
initialDialog?: GenerateDialogState;
onRequestUpload?: (target: UploadTarget) => void;
onSubmit?: (dialog: GenerateDialogState) => void;
}) {
const [dialog, setDialog] = useState<GenerateDialogState | null>(
initialDialog,
);
const [isReferenceMenuOpen, setIsReferenceMenuOpen] = useState(false);
const [isPickingReference, setIsPickingReference] = useState(false);
const generationReferenceButtonRef = createRef<HTMLButtonElement>();
return dialog ? (
<div>
<ImageCanvasBasicGenerationComposerView
dialog={dialog}
style={{ left: 10, top: 20 }}
setGenerateDialog={setDialog}
generationReferenceButtonRef={generationReferenceButtonRef}
isGenerationReferenceMenuOpen={isReferenceMenuOpen}
setIsGenerationReferenceMenuOpen={setIsReferenceMenuOpen}
setIsPickingGenerationReferenceFromCanvas={setIsPickingReference}
renderEditorPortal={(node) => node}
buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })}
onRequestUpload={onRequestUpload}
onToggleReferenceMenu={() => setIsReferenceMenuOpen((open) => !open)}
onSubmit={onSubmit}
/>
<output aria-label="当前提示词">{dialog.prompt}</output>
<output aria-label="当前比例">{dialog.aspectRatio}</output>
<output aria-label="当前尺寸">{dialog.imageSize}</output>
<output aria-label="当前模型">{dialog.imageModel}</output>
<output aria-label="当前状态">{dialog.status}</output>
<output aria-label="当前错误">{dialog.errorMessage ?? '-'}</output>
<output aria-label="选择参考图">{String(isPickingReference)}</output>
</div>
) : null;
}
describe('ImageCanvasBasicGenerationComposerView', () => {
it('updates prompt, clears failed state, uploads references and submits', () => {
const requestUpload = vi.fn();
const submitGeneration = vi.fn();
render(
<BasicGenerationHarness
initialDialog={createDialog({
status: 'failed',
errorMessage: '生成失败',
})}
onRequestUpload={requestUpload}
onSubmit={submitGeneration}
/>,
);
fireEvent.change(screen.getByLabelText('生成提示词'), {
target: { value: '新的提示' },
});
const panel = screen.getByRole('dialog', { name: '生成图片' });
fireEvent.click(within(panel).getByRole('button', { name: '添加参考图' }));
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect(screen.getByLabelText('当前提示词').textContent).toBe('新的提示');
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
expect(requestUpload).toHaveBeenCalledWith('generation-reference');
expect(submitGeneration).toHaveBeenCalledWith(
expect.objectContaining({ prompt: '新的提示', status: 'idle' }),
);
});
it('keeps references on the first row and appends a new add-reference entry', () => {
render(
<BasicGenerationHarness
initialDialog={createDialog({
generationReferences: [
{
id: 'ref-a',
label: '参考图A',
src: 'data:image/png;base64,cmVmQQ==',
},
{
id: 'ref-b',
label: '参考图B',
src: 'data:image/png;base64,cmVmQg==',
},
],
})}
/>,
);
const panel = screen.getByRole('dialog', { name: '生成图片' });
const strip = panel.querySelector('.image-canvas-editor__reference-strip');
expect(strip).toBeTruthy();
expect(within(strip as HTMLElement).getByText('参考图A')).toBeTruthy();
expect(within(strip as HTMLElement).getByText('参考图B')).toBeTruthy();
expect(
within(strip as HTMLElement).getByRole('button', { name: '添加参考图' }),
).toBeTruthy();
});
it('removes an existing reference from its hover delete button', () => {
render(
<BasicGenerationHarness
initialDialog={createDialog({
generationReferences: [
{
id: 'ref-a',
label: '参考图A',
src: 'data:image/png;base64,cmVmQQ==',
},
],
})}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '删除参考图A' }));
expect(screen.queryByText('参考图A')).toBeNull();
});
it('shows only current image options and opens upward option panels', () => {
render(<BasicGenerationHarness />);
const panel = screen.getByRole('dialog', { name: '生成图片' });
fireEvent.click(
within(panel).getByRole('button', {
name: '生成图片尺寸 16:9·1K',
}),
);
const dimensionPanel = screen.getByRole('menu', {
name: '生成图片尺寸选项',
});
expect(within(dimensionPanel).getByText('比例')).toBeTruthy();
fireEvent.click(within(dimensionPanel).getByRole('button', { name: '2K' }));
expect(screen.getByLabelText('当前尺寸').textContent).toBe('2K');
fireEvent.click(
within(panel).getByRole('button', {
name: '生成图片模型 nanobanana2',
}),
);
const modelPanel = screen.getByRole('menu', { name: '生成图片模型选项' });
fireEvent.click(
within(modelPanel).getByRole('button', { name: /gpt-image-2/ }),
);
expect(screen.getByLabelText('当前模型').textContent).toBe('gpt-image-2');
const submitButton = within(panel).getByRole('button', { name: '生成' });
expect(submitButton.textContent).toBe('生成5泥点');
});
it('clicks the parent generation panel to collapse an opened option panel', () => {
render(<BasicGenerationHarness />);
const panel = screen.getByRole('dialog', { name: '生成图片' });
fireEvent.click(
within(panel).getByRole('button', {
name: '生成图片尺寸 16:9·1K',
}),
);
expect(
screen.getByRole('menu', { name: '生成图片尺寸选项' }),
).toBeTruthy();
fireEvent.click(screen.getByRole('textbox', { name: '生成提示词' }));
expect(
screen.queryByRole('menu', { name: '生成图片尺寸选项' }),
).toBeNull();
});
it('does not render a standalone close button', () => {
render(<BasicGenerationHarness />);
expect(
screen.queryByRole('button', { name: '关闭生成图片' }),
).toBeNull();
});
});