Files
Genarrative/src/components/big-fish-result/BigFishResultView.test.tsx
kdletters cb01d33944 收口个人中心提示与大鱼摘要标签
将个人中心邀请弹窗奖励说明迁移到共享状态提示组件
将大鱼吃小鱼结果页 hero 摘要标签迁移到共享胶囊标签组件
补充充值商品卡购买胶囊暂不抽共享组件的收口文档与团队决策
2026-06-10 16:24:53 +08:00

439 lines
14 KiB
TypeScript

// @vitest-environment jsdom
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish';
import { BigFishResultView } from './BigFishResultView';
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: ({
src,
alt,
className,
}: {
src?: string | null;
alt?: string;
className?: string;
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
}));
function findNearestClassName(
element: HTMLElement,
classNamePart: string,
): HTMLElement | null {
let current: HTMLElement | null = element;
while (current) {
if (current.className.includes(classNamePart)) {
return current;
}
current = current.parentElement;
}
return null;
}
function createSession(): BigFishSessionSnapshotResponse {
return {
sessionId: 'big-fish-session-1',
currentTurn: 2,
progressPercent: 88,
stage: 'asset_refining',
anchorPack: {
gameplayPromise: {
key: 'gameplayPromise',
label: '玩法承诺',
value: '弱小逆袭',
status: 'confirmed',
},
ecologyVisualTheme: {
key: 'ecologyVisualTheme',
label: '生态与视觉母题',
value: '深海谜境',
status: 'confirmed',
},
growthLadder: {
key: 'growthLadder',
label: '成长阶梯',
value: '8 级进化',
status: 'confirmed',
},
riskTempo: {
key: 'riskTempo',
label: '风险节奏',
value: '平衡',
status: 'confirmed',
},
},
draft: {
title: '深海谜境',
subtitle: '逐级吞噬成长',
coreFun: '弱小逆袭',
ecologyTheme: '深海谜境',
levels: [
{
level: 1,
name: '荧潮幼体',
oneLineFantasy: '在深海荧光裂谷中寻找第一个同伴。',
textDescription:
'荧潮幼体是深海谜境里的初始个体,体型最小,会先谨慎试探并寻找可吞噬目标。',
silhouetteDirection: '圆润鱼苗',
sizeRatio: 1,
visualDescription:
'带有浅青色荧光纹路的小型鱼苗,轮廓圆润,呈现弱小但灵动的开局形象。',
visualPromptSeed: '深海荧光幼体',
idleMotionDescription:
'待机时轻微漂浮,尾鳍做小幅摆动,像是在观察周围海流。',
moveMotionDescription:
'移动时身体前探,尾鳍清晰摆尾推进,呈现连续游动感。',
motionPromptSeed: '轻微摆尾',
mergeSourceLevel: null,
preyWindow: [1],
threatWindow: [2],
isFinalLevel: false,
},
],
background: {
theme: '深海谜境',
colorMood: '深蓝与青绿',
foregroundHints: '漂浮微粒',
midgroundComposition: '中央留白',
backgroundDepth: '深海纵深',
safePlayAreaHint: '中央 70%',
spawnEdgeHint: '四周边缘',
backgroundPromptSeed: '深海谜境背景',
},
runtimeParams: {
levelCount: 1,
mergeCountPerUpgrade: 3,
spawnTargetCount: 12,
leaderMoveSpeed: 160,
followerCatchUpSpeed: 120,
offscreenCullSeconds: 3,
preySpawnDeltaLevels: [1],
threatSpawnDeltaLevels: [1],
winLevel: 1,
},
},
assetSlots: [
{
slotId: 'big-fish-asset-level-main',
assetKind: 'level_main_image',
level: 1,
motionKey: null,
status: 'ready',
assetUrl:
'/generated-big-fish-assets/big-fish-session-1/level-main-image/level-1/image.png',
promptSnapshot: '深海荧光幼体',
updatedAt: '2026-04-23T10:00:00.000Z',
},
{
slotId: 'big-fish-asset-background',
assetKind: 'stage_background',
level: null,
motionKey: null,
status: 'ready',
assetUrl:
'/generated-big-fish-assets/big-fish-session-1/stage-background/image.png',
promptSnapshot: '深海谜境背景',
updatedAt: '2026-04-23T10:00:00.000Z',
},
],
assetCoverage: {
levelMainImageReadyCount: 1,
levelMotionReadyCount: 0,
backgroundReady: true,
requiredLevelCount: 1,
publishReady: false,
blockers: ['还缺少 2 个基础动作'],
},
messages: [],
lastAssistantReply: '主图占位图已生成。',
publishReady: false,
updatedAt: '2026-04-23T10:00:00.000Z',
};
}
describe('BigFishResultView', () => {
test('uses PlatformEmptyState chrome when draft is missing', () => {
render(
<BigFishResultView
session={{
...createSession(),
draft: null,
}}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
const emptyState = screen.getByText('还没有可编辑的玩法草稿');
expect(emptyState.className).toContain('platform-empty-state');
expect(emptyState.className).toContain('bg-white/74');
expect(emptyState.className).toContain('text-[var(--platform-text-base)]');
});
test('renders generated formal previews with accurate status copy', () => {
render(
<BigFishResultView
session={createSession()}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
expect(screen.getByText('主图 已生成')).toBeTruthy();
const levelImage = screen.getByAltText('荧潮幼体');
expect(levelImage).toBeTruthy();
const levelFrame = findNearestClassName(levelImage, 'relative');
expect(levelFrame?.className).toContain('aspect-square');
expect(levelFrame?.className).toContain('radial-gradient');
expect(levelFrame?.className).toContain('linear-gradient');
expect(levelFrame?.className).not.toContain(
'bg-[var(--platform-subpanel-fill)]',
);
const backgroundImage = screen.getByAltText('深海谜境 场地背景');
expect(backgroundImage).toBeTruthy();
const backgroundFrame = findNearestClassName(backgroundImage, 'relative');
expect(backgroundFrame?.className).toContain('aspect-[9/16]');
expect(backgroundFrame?.className).toContain('radial-gradient');
expect(backgroundFrame?.className).toContain('linear-gradient');
expect(backgroundFrame?.className).not.toContain(
'bg-[var(--platform-subpanel-fill)]',
);
expect(
findNearestClassName(screen.getByText('荧潮幼体'), 'platform-subpanel')
?.className,
).toContain('rounded-[1.5rem]');
for (const label of ['猎物 1', '威胁 2', '主图 已生成']) {
const badge = screen.getByText(label);
expect(badge.className).toContain('rounded-full');
expect(badge.className).toContain(
'bg-[var(--platform-subpanel-fill)]',
);
}
expect(
findNearestClassName(screen.getByText('场地背景'), 'platform-subpanel')
?.className,
).toContain('rounded-[1.5rem]');
expect(
findNearestClassName(screen.getByText('发布校验'), 'platform-subpanel')
?.className,
).toContain('rounded-[1.5rem]');
const blockerStatus = findNearestClassName(
screen.getByText('还缺少 2 个基础动作'),
'platform-status-message',
);
expect(blockerStatus?.className).toContain('platform-status-message');
expect(blockerStatus?.className).toContain(
'border-[var(--platform-warm-border)]',
);
expect(blockerStatus?.className).toContain(
'bg-[var(--platform-warm-bg)]',
);
for (const label of ['弱小逆袭', '深海谜境', '1 级']) {
const badge = screen
.getAllByText(label)
.find((element) => element.className.includes('inline-flex'));
if (!badge) {
throw new Error(`missing hero badge for ${label}`);
}
expect(badge.className).toContain('inline-flex');
expect(badge.className).toContain('rounded-full');
expect(badge.className).toContain('border-transparent');
}
});
test('uses platform pill badge for ready publish status', () => {
render(
<BigFishResultView
session={{
...createSession(),
publishReady: true,
assetCoverage: {
levelMainImageReadyCount: 1,
levelMotionReadyCount: 2,
backgroundReady: true,
requiredLevelCount: 1,
publishReady: true,
blockers: [],
},
}}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
const readyBadge = screen.getByText('已达到发布条件');
expect(readyBadge.tagName).toBe('SPAN');
expect(readyBadge.className).toContain('rounded-full');
expect(readyBadge.className).toContain('border-emerald-200');
expect(readyBadge.className).toContain('bg-emerald-50');
});
test('uses level descriptions as default prompt content in asset studio', () => {
render(
<BigFishResultView
session={createSession()}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '主图' }));
expect(screen.getByText('PROMPT').className).toContain('tracking-[0.18em]');
const studioPreviewFrame = findNearestClassName(
screen.getByAltText('Lv.1 主图工坊'),
'relative',
);
expect(studioPreviewFrame?.className).toContain('aspect-[9/5]');
expect(studioPreviewFrame?.className).toContain('border-dashed');
expect(studioPreviewFrame?.className).toContain('bg-cyan-50/40');
expect(studioPreviewFrame?.className).not.toContain(
'bg-[var(--platform-subpanel-fill)]',
);
expect(
screen.getByText(
'带有浅青色荧光纹路的小型鱼苗,轮廓圆润,呈现弱小但灵动的开局形象。',
),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
fireEvent.click(screen.getByRole('button', { name: '待机' }));
expect(
screen.getByText('待机时轻微漂浮,尾鳍做小幅摆动,像是在观察周围海流。'),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
fireEvent.click(screen.getByRole('button', { name: '移动' }));
expect(
screen.getByText('移动时身体前探,尾鳍清晰摆尾推进,呈现连续游动感。'),
).toBeTruthy();
});
test('uses PlatformActionButton chrome for white surface asset actions', () => {
render(
<BigFishResultView
session={createSession()}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
for (const actionName of ['主图', '待机', '移动', '生成背景']) {
const action = screen.getByRole('button', { name: actionName });
expect(action.className).toContain('platform-button');
expect(action.className).toContain('rounded-full');
}
fireEvent.click(screen.getByRole('button', { name: '主图' }));
for (const actionName of ['关闭', '生成并应用正式图']) {
const action = screen.getByRole('button', { name: actionName });
expect(action.className).toContain('platform-button');
expect(action.className).toContain('rounded-full');
}
});
test('reuses shared hero action chrome for top-level result actions', () => {
render(
<BigFishResultView
session={createSession()}
onBack={() => {}}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
expect(screen.getByRole('button', { name: '返回' }).className).toContain(
'rounded-full',
);
expect(screen.getByRole('button', { name: '测试' }).className).toContain(
'platform-action-button--editor-dark',
);
expect(screen.getByRole('button', { name: '发布' }).className).toContain(
'platform-action-button--editor-dark',
);
});
test('shows publish failures in a dismissible modal', () => {
const onDismissError = vi.fn();
render(
<BigFishResultView
session={createSession()}
error="big_fish 发布校验未通过:还缺少 16 个基础动作"
onBack={() => {}}
onDismissError={onDismissError}
onExecuteAction={() => {}}
onStartTestRun={() => {}}
/>,
);
expect(screen.getByRole('dialog')).toBeTruthy();
expect(screen.getByText('发布失败')).toBeTruthy();
expect(
screen.getByText('big_fish 发布校验未通过:还缺少 16 个基础动作'),
).toBeTruthy();
const iconBadge = screen.getByLabelText('发布失败提示');
expect(iconBadge.className).toContain(
'bg-[var(--platform-button-danger-fill)]',
);
expect(iconBadge.className).toContain(
'text-[var(--platform-button-danger-text)]',
);
fireEvent.click(screen.getByRole('button', { name: '知道了' }));
expect(onDismissError).toHaveBeenCalledTimes(1);
});
test('shows published state and prevents duplicate publish clicks', () => {
const onExecuteAction = vi.fn();
render(
<BigFishResultView
session={{
...createSession(),
stage: 'published',
publishReady: true,
assetCoverage: {
levelMainImageReadyCount: 1,
levelMotionReadyCount: 2,
backgroundReady: true,
requiredLevelCount: 1,
publishReady: true,
blockers: [],
},
}}
onBack={() => {}}
onExecuteAction={onExecuteAction}
onStartTestRun={() => {}}
/>,
);
const publishedButton = screen.getByRole('button', { name: '已发布' });
expect((publishedButton as HTMLButtonElement).disabled).toBe(true);
expect(screen.getAllByText('已发布').length).toBeGreaterThan(0);
const publishedBadge = screen
.getAllByText('已发布')
.find((element) => element.tagName === 'SPAN');
expect(publishedBadge?.className).toContain('border-emerald-200');
expect(publishedBadge?.className).toContain('bg-emerald-50');
fireEvent.click(publishedButton);
expect(onExecuteAction).not.toHaveBeenCalled();
});
});