1
This commit is contained in:
425
src/components/puzzle-result/PuzzleResultView.test.tsx
Normal file
425
src/components/puzzle-result/PuzzleResultView.test.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import { puzzleAssetClient } from '../../services/puzzle-works/puzzleAssetClient';
|
||||
import { PuzzleResultView } from './PuzzleResultView';
|
||||
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
}: {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-works/puzzleAssetClient', () => ({
|
||||
puzzleAssetClient: {
|
||||
listHistoryAssets: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function createSession(
|
||||
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
|
||||
): PuzzleAgentSessionSnapshot {
|
||||
const baseSession: PuzzleAgentSessionSnapshot = {
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 2,
|
||||
progressPercent: 88,
|
||||
stage: 'ready_to_publish',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'themePromise',
|
||||
label: '题材承诺',
|
||||
value: '雨夜猫咪',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visualSubject',
|
||||
label: '画面主体',
|
||||
value: '屋檐下的猫',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visualMood',
|
||||
label: '视觉气质',
|
||||
value: '温暖',
|
||||
status: 'confirmed',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'compositionHooks',
|
||||
label: '拼图记忆点',
|
||||
value: '雨滴与灯牌',
|
||||
status: 'confirmed',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tagsAndForbidden',
|
||||
label: '标签与禁忌',
|
||||
value: '猫咪、雨夜',
|
||||
status: 'confirmed',
|
||||
},
|
||||
},
|
||||
draft: {
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
themeTags: ['猫咪', '雨夜'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'themePromise',
|
||||
label: '题材承诺',
|
||||
value: '雨夜猫咪',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visualSubject',
|
||||
label: '画面主体',
|
||||
value: '屋檐下的猫',
|
||||
status: 'confirmed',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visualMood',
|
||||
label: '视觉气质',
|
||||
value: '温暖',
|
||||
status: 'confirmed',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'compositionHooks',
|
||||
label: '拼图记忆点',
|
||||
value: '雨滴与灯牌',
|
||||
status: 'confirmed',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tagsAndForbidden',
|
||||
label: '标签与禁忌',
|
||||
value: '猫咪、雨夜',
|
||||
status: 'confirmed',
|
||||
},
|
||||
},
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/candidate-1.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫咪',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/candidate-1.png',
|
||||
coverAssetId: 'asset-1',
|
||||
generationStatus: 'ready',
|
||||
metadata: null,
|
||||
},
|
||||
messages: [],
|
||||
lastAssistantReply: null,
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-26T10:00:00.000Z',
|
||||
};
|
||||
|
||||
const session = {
|
||||
...baseSession,
|
||||
resultPreview: {
|
||||
draft: baseSession.draft!,
|
||||
publishReady: true,
|
||||
blockers: [],
|
||||
qualityFindings: [],
|
||||
},
|
||||
...overrides,
|
||||
} satisfies PuzzleAgentSessionSnapshot;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
describe('PuzzleResultView', () => {
|
||||
test('uses two tabs without author preview or persistent publish validation', () => {
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '基本信息' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '拼图图片' })).toBeTruthy();
|
||||
expect(screen.queryByText('作者预览')).toBeNull();
|
||||
expect(screen.queryByText('发布校验')).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /作品测试/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /发布/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('edits theme tags with chips instead of a persistent tag input', () => {
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('新题材标签')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('删除标签 猫咪'));
|
||||
expect(screen.queryByText('猫咪')).toBeNull();
|
||||
expect(screen.getByText('雨夜')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('新增题材标签'));
|
||||
fireEvent.change(screen.getByLabelText('新题材标签'), {
|
||||
target: { value: '暖灯' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加' }));
|
||||
|
||||
expect(screen.getByText('暖灯')).toBeTruthy();
|
||||
expect(screen.queryByLabelText('新题材标签')).toBeNull();
|
||||
});
|
||||
|
||||
test('shows blockers only after clicking publish and blocks publish action', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
resultPreview: {
|
||||
draft: createSession().draft!,
|
||||
publishReady: false,
|
||||
blockers: [
|
||||
{
|
||||
id: 'missing-cover',
|
||||
code: 'missing-cover',
|
||||
message: '请先选择正式图',
|
||||
},
|
||||
],
|
||||
qualityFindings: [],
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={onExecuteAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('请先选择正式图')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /发布/u }));
|
||||
const dialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
expect(within(dialog).getByText('请先选择正式图')).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '发布到广场' }));
|
||||
expect(onExecuteAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('starts work test from the current editable draft', () => {
|
||||
const onStartTestRun = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onStartTestRun={onStartTestRun}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue('雨夜猫街'), {
|
||||
target: { value: '暖灯猫街' },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText('新增题材标签'));
|
||||
fireEvent.change(screen.getByLabelText('新题材标签'), {
|
||||
target: { value: '暖灯' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /作品测试/u }));
|
||||
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
levelName: '暖灯猫街',
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('generates one image from the picture description and replaces current image', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={onExecuteAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拼图图片' }));
|
||||
expect(screen.getByText('画面描述')).toBeTruthy();
|
||||
expect(screen.queryByText(/候选图/u)).toBeNull();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('画面描述'), {
|
||||
target: { value: '一只猫在雨夜灯牌下回头。' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成并替换当前图片/u }));
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'generate_puzzle_images',
|
||||
promptText: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: undefined,
|
||||
candidateCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('selects a history puzzle asset as reference image for the next generation', async () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
vi.mocked(puzzleAssetClient.listHistoryAssets).mockResolvedValue([
|
||||
{
|
||||
assetObjectId: 'asset-history-1',
|
||||
assetKind: 'puzzle_cover_image',
|
||||
imageSrc: '/generated-puzzle-assets/history/image.png',
|
||||
ownerUserId: 'user-1',
|
||||
ownerLabel: '账号 user-1',
|
||||
profileId: null,
|
||||
entityId: 'puzzle-session-1',
|
||||
createdAt: '2026-04-27T10:00:00.000Z',
|
||||
updatedAt: '2026-04-27T10:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={onExecuteAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拼图图片' }));
|
||||
fireEvent.click(screen.getByLabelText('从历史拼图素材库选择'));
|
||||
|
||||
const dialog = await screen.findByRole('dialog', {
|
||||
name: '选择历史拼图素材',
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /账号 user-1/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '选择历史拼图素材' })).toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成并替换当前图片/u }));
|
||||
|
||||
expect(onExecuteAction).toHaveBeenLastCalledWith({
|
||||
action: 'generate_puzzle_images',
|
||||
promptText: '屋檐下的猫与暖灯街角。',
|
||||
referenceImageSrc: '/generated-puzzle-assets/history/image.png',
|
||||
candidateCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('refreshes the current formal image when session cover image changes', async () => {
|
||||
const { rerender } = render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拼图图片' }));
|
||||
expect(screen.getByRole('img', { name: '雨夜猫街' }).getAttribute('src')).toBe(
|
||||
'/puzzle/candidate-1.png',
|
||||
);
|
||||
|
||||
rerender(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...createSession().draft!,
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-2',
|
||||
imageSrc: '/puzzle/candidate-2.png',
|
||||
assetId: 'asset-2',
|
||||
prompt: '新图',
|
||||
actualPrompt: '新图',
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-2',
|
||||
coverImageSrc: '/puzzle/candidate-2.png',
|
||||
coverAssetId: 'asset-2',
|
||||
},
|
||||
updatedAt: '2026-04-27T11:11:11.000Z',
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img', { name: '雨夜猫街' }).getAttribute('src')).toBe(
|
||||
'/puzzle/candidate-2.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('prefers the selected latest candidate image when coverImageSrc lags behind', async () => {
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...createSession().draft!,
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/candidate-1.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '旧图',
|
||||
actualPrompt: '旧图',
|
||||
sourceType: 'generated',
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
candidateId: 'candidate-2',
|
||||
imageSrc: '/puzzle/candidate-2.png',
|
||||
assetId: 'asset-2',
|
||||
prompt: '新图',
|
||||
actualPrompt: '新图',
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-2',
|
||||
coverImageSrc: '/puzzle/candidate-1.png',
|
||||
coverAssetId: 'asset-1',
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拼图图片' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img', { name: '雨夜猫街' }).getAttribute('src')).toBe(
|
||||
'/puzzle/candidate-2.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user