This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -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,
);
});