// @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 ? {alt} : 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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( {}} 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(); }); });