// @vitest-environment jsdom import { act, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, test, vi } from 'vitest'; import type { BigFishRuntimeSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish'; import { BigFishRuntimeShell } from './BigFishRuntimeShell'; vi.mock('../ResolvedAssetImage', () => ({ ResolvedAssetImage: ({ src, alt, className, }: { src?: string | null; alt?: string; className?: string; }) => (src ? {alt} : null), })); function createRun( status: BigFishRuntimeSnapshotResponse['status'], ): BigFishRuntimeSnapshotResponse { return { runId: 'big-fish-run-1', sessionId: 'big-fish-session-1', status, tick: 18, playerLevel: 2, winLevel: 5, leaderEntityId: null, ownedEntities: [], wildEntities: [], cameraCenter: { x: 0, y: 0 }, lastInput: { x: 0, y: 0 }, eventLog: ['己方鱼群已经耗尽'], updatedAt: '2026-04-26T12:00:00.000Z', }; } function dispatchPointerEvent( target: HTMLElement, type: string, options: { pointerId: number; clientX: number; clientY: number }, ) { const event = new Event(type, { bubbles: true, cancelable: true }); Object.assign(event, options); target.dispatchEvent(event); } describe('BigFishRuntimeShell', () => { afterEach(() => { vi.useRealTimers(); }); test('renders restart and exit actions after a failed run', () => { const onBack = vi.fn(); const onRestart = vi.fn(); render( {}} />, ); fireEvent.click(screen.getByRole('button', { name: '重来' })); fireEvent.click(screen.getByRole('button', { name: '退出' })); expect(screen.getByText('本轮失败')).toBeTruthy(); expect(onRestart).toHaveBeenCalledTimes(1); expect(onBack).toHaveBeenCalledTimes(1); }); test('keeps an exit action after a won run', () => { const onBack = vi.fn(); render( {}} />, ); expect(screen.getByText('通关完成')).toBeTruthy(); expect(screen.queryByRole('button', { name: '重来' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '退出' })); expect(onBack).toHaveBeenCalledTimes(1); }); test('opens and closes the runtime rule modal', () => { render( {}} onSubmitInput={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '查看规则' })); expect(screen.getByRole('dialog', { name: '玩法规则' })).toBeTruthy(); expect(screen.getByText('低级或同级野生实体会被收编。')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '关闭' })); expect(screen.queryByRole('dialog', { name: '玩法规则' })).toBeNull(); }); test('keeps moving in the last sampled direction after drag ends', () => { vi.useFakeTimers(); const onSubmitInput = vi.fn(); const { container } = render( {}} onSubmitInput={onSubmitInput} />, ); const stage = container.querySelector('.touch-none'); if (!(stage instanceof HTMLElement)) { throw new Error('Missing big fish stage'); } act(() => { dispatchPointerEvent(stage, 'pointerdown', { pointerId: 1, clientX: 100, clientY: 100, }); }); act(() => { dispatchPointerEvent(stage, 'pointermove', { pointerId: 1, clientX: 140, clientY: 100, }); }); act(() => { vi.advanceTimersByTime(100); }); act(() => { dispatchPointerEvent(stage, 'pointerup', { pointerId: 1, clientX: 140, clientY: 100, }); }); act(() => { vi.advanceTimersByTime(220); }); expect(onSubmitInput).toHaveBeenLastCalledWith({ x: 1, y: 0 }); }); });