1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
@@ -68,8 +68,61 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function stubReferenceImageUpload(dataUrl: string, width = 512, height = 512) {
|
||||
class MockFileReader {
|
||||
result: string | null = null;
|
||||
onload: null | (() => void) = null;
|
||||
onerror: null | (() => void) = null;
|
||||
|
||||
readAsDataURL() {
|
||||
this.result = dataUrl;
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
|
||||
class MockImage {
|
||||
onload: null | (() => void) = null;
|
||||
onerror: null | (() => void) = null;
|
||||
naturalWidth = width;
|
||||
naturalHeight = height;
|
||||
width = width;
|
||||
height = height;
|
||||
|
||||
set src(_value: string) {
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||||
vi.stubGlobal('Image', MockImage as unknown as typeof Image);
|
||||
}
|
||||
|
||||
function stubCanvas(dataUrl: string, drawImage = vi.fn()) {
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
|
||||
if (tagName !== 'canvas') {
|
||||
return originalCreateElement(tagName);
|
||||
}
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => ({
|
||||
drawImage,
|
||||
fillRect: vi.fn(),
|
||||
fillStyle: '',
|
||||
imageSmoothingEnabled: false,
|
||||
imageSmoothingQuality: 'low',
|
||||
}),
|
||||
toDataURL: vi.fn(() => dataUrl),
|
||||
} as unknown as HTMLCanvasElement;
|
||||
});
|
||||
return drawImage;
|
||||
}
|
||||
|
||||
test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
|
||||
@@ -85,8 +138,9 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
|
||||
expect(screen.queryByLabelText('作品名称')).toBeNull();
|
||||
expect(screen.queryByLabelText('作品描述')).toBeNull();
|
||||
expect(screen.getByText('创建拼图')).toBeTruthy();
|
||||
expect(screen.getByText('想做个什么玩法?')).toBeTruthy();
|
||||
expect(screen.queryByText('try')).toBeNull();
|
||||
expect(screen.queryByText('Template')).toBeNull();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('画面描述'), {
|
||||
target: { value: '一只猫在雨夜灯牌下回头。' },
|
||||
@@ -98,16 +152,16 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
});
|
||||
expect(screen.getByText('消耗2光点')).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||
expect(screen.queryByText('旧会话消息不再渲染为聊天入口。')).toBeNull();
|
||||
});
|
||||
|
||||
test('puzzle workspace applies a creation template prompt', () => {
|
||||
test('puzzle workspace keeps the reference image upload as a primary panel', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
|
||||
render(
|
||||
const { container } = render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
@@ -117,22 +171,66 @@ test('puzzle workspace applies a creation template prompt', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '宠物可爱拼图模板' }));
|
||||
const uploadInput = screen.getByLabelText('上传拼图图片', {
|
||||
selector: 'input',
|
||||
});
|
||||
const uploadCard = uploadInput.closest('.puzzle-image-upload-card');
|
||||
expect(uploadCard).not.toBeNull();
|
||||
expect(uploadCard?.closest('.platform-subpanel')).toBeNull();
|
||||
expect(container.querySelector('.puzzle-image-upload-card')).toBeTruthy();
|
||||
|
||||
expect((screen.getByLabelText('画面描述') as HTMLTextAreaElement).value).toBe(
|
||||
'一只可爱的橘猫趴在阳光窗台上,旁边有绿植、毛线球和小毯子,猫的表情清楚,画面温柔干净,适合萌宠拼图分享。',
|
||||
expect(screen.getByText('拼图画面')).toBeTruthy();
|
||||
expect(screen.getByText('点击上传拼图图片')).toBeTruthy();
|
||||
expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull();
|
||||
expect(screen.queryByLabelText('拼图创作模板')).toBeNull();
|
||||
expect(
|
||||
(screen.getByLabelText('画面描述') as HTMLTextAreaElement).value,
|
||||
).toBe('');
|
||||
expect(
|
||||
(screen.getByLabelText('画面描述') as HTMLTextAreaElement).placeholder,
|
||||
).toBe('');
|
||||
expect(screen.queryByText(/一只猫在雨夜灯牌下回头/u)).toBeNull();
|
||||
expect(screen.getByLabelText('画面描述').className).toContain(
|
||||
'min-h-[clamp(5rem,15svh,7rem)]',
|
||||
);
|
||||
expect(screen.getAllByText('宠物可爱拼图').length).toBeGreaterThan(1);
|
||||
expect(uploadCard?.className).toContain('aspect-square');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('画面描述'), {
|
||||
target: { value: '一只猫在阳光窗台上看着毛线球。' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成草稿/u }));
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pictureDescription:
|
||||
'一只可爱的橘猫趴在阳光窗台上,旁边有绿植、毛线球和小毯子,猫的表情清楚,画面温柔干净,适合萌宠拼图分享。',
|
||||
pictureDescription: '一只猫在阳光窗台上看着毛线球。',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('puzzle upload card stays light in light theme', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
const { container } = render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={onCreateFromForm}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.puzzle-image-upload-card')).toBeTruthy();
|
||||
const uploadLabel = screen.getByText('点击上传拼图图片');
|
||||
expect(uploadLabel).toBeTruthy();
|
||||
expect(uploadLabel.closest('.puzzle-image-upload-card')).toBeNull();
|
||||
expect(screen.queryByText('AI重绘')).toBeNull();
|
||||
expect(container.querySelector('.puzzle-image-upload-card')?.className).toContain(
|
||||
'bg-white/90',
|
||||
);
|
||||
expect(container.querySelector('.puzzle-image-upload-card')?.className).not.toContain(
|
||||
'bg-slate-950',
|
||||
);
|
||||
});
|
||||
|
||||
test('puzzle workspace falls back to compile action for restored sessions', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
const onCreateFromForm = vi.fn();
|
||||
@@ -156,6 +254,7 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
|
||||
promptText: '潮雾中的灯塔与断桥',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
candidateCount: 1,
|
||||
});
|
||||
});
|
||||
@@ -236,9 +335,9 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect((screen.getByLabelText('画面描述') as HTMLTextAreaElement).value).toBe(
|
||||
'旧街灯牌下的猫。',
|
||||
);
|
||||
expect(
|
||||
(screen.getByLabelText('画面描述') as HTMLTextAreaElement).value,
|
||||
).toBe('旧街灯牌下的猫。');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('画面描述'), {
|
||||
target: { value: '旧街灯牌下的猫和发光雨伞。' },
|
||||
@@ -253,5 +352,125 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
|
||||
pictureDescription: '旧街灯牌下的猫和发光雨伞。',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle workspace hides prompt and cost when AI redraw is off', async () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
const uploadedDataUrl = 'data:image/png;base64,uploaded-square';
|
||||
stubReferenceImageUpload(uploadedDataUrl);
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={onCreateFromForm}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('上传拼图图片', {
|
||||
selector: 'input',
|
||||
});
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
files: [new File(['x'], 'first-level.png', { type: 'image/png' })],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('画面AI重绘要求(提示词)')).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText('first-level.png')).toBeNull();
|
||||
const aiRedrawSwitch = screen.getByRole('switch', { name: 'AI重绘' });
|
||||
expect((aiRedrawSwitch as HTMLInputElement).checked).toBe(true);
|
||||
fireEvent.click(aiRedrawSwitch);
|
||||
expect(screen.queryByLabelText('画面AI重绘要求(提示词)')).toBeNull();
|
||||
expect(screen.queryByText('消耗2光点')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成草稿/u }));
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: 'first-level.png',
|
||||
pictureDescription: 'first-level.png',
|
||||
referenceImageSrc: uploadedDataUrl,
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle workspace shows AI redraw switch only after upload', async () => {
|
||||
const uploadedDataUrl = 'data:image/png;base64,uploaded-square';
|
||||
stubReferenceImageUpload(uploadedDataUrl);
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('switch', { name: 'AI重绘' })).toBeNull();
|
||||
fireEvent.change(screen.getByLabelText('上传拼图图片', { selector: 'input' }), {
|
||||
target: {
|
||||
files: [new File(['x'], 'first-level.png', { type: 'image/png' })],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('switch', { name: 'AI重绘' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle workspace opens crop tool for non-square uploads', async () => {
|
||||
const sourceDataUrl = 'data:image/png;base64,wide-source';
|
||||
const croppedDataUrl = 'data:image/jpeg;base64,cropped-square';
|
||||
stubReferenceImageUpload(sourceDataUrl, 800, 600);
|
||||
const drawImage = stubCanvas(croppedDataUrl);
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(
|
||||
screen.getByLabelText('上传拼图图片', { selector: 'input' }),
|
||||
{
|
||||
target: {
|
||||
files: [new File(['x'], 'wide.png', { type: 'image/png' })],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog', { name: '裁剪拼图图片' })).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '应用' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '裁剪拼图图片' })).toBeNull();
|
||||
});
|
||||
expect(screen.queryByText('wide.png')).toBeNull();
|
||||
expect(screen.getByAltText('拼图图片')).toBeTruthy();
|
||||
expect(drawImage).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
100,
|
||||
0,
|
||||
600,
|
||||
600,
|
||||
0,
|
||||
0,
|
||||
600,
|
||||
600,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user