将个人中心邀请弹窗奖励说明迁移到共享状态提示组件 将大鱼吃小鱼结果页 hero 摘要标签迁移到共享胶囊标签组件 补充充值商品卡购买胶囊暂不抽共享组件的收口文档与团队决策
439 lines
14 KiB
TypeScript
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();
|
|
});
|
|
});
|