// @vitest-environment jsdom import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, test, vi } from 'vitest'; import type { BigFishRuntimeSnapshotResponse } from '../../../packages/shared/src/contracts/bigFish'; import * as clipboardService from '../../services/clipboard'; import { BigFishRuntimeShell } from './BigFishRuntimeShell'; vi.mock('../ResolvedAssetImage', () => ({ ResolvedAssetImage: ({ src, alt, className, }: { src?: string | null; alt?: string; className?: string; }) => (src ? {alt} : null), })); vi.mock('../../services/clipboard', () => ({ copyTextToClipboard: vi.fn(), })); 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); } afterEach(() => { vi.clearAllMocks(); }); describe('BigFishRuntimeShell', () => { 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('copies public work share text through unified feedback', async () => { vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValue(true); render( {}} onSubmitInput={() => {}} />, ); fireEvent.click(screen.getByRole('button', { name: '分享作品' })); await waitFor(() => { expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith( expect.stringContaining('邀请你来玩《深海追击》'), ); }); const copiedText = vi.mocked(clipboardService.copyTextToClipboard).mock .calls[0]?.[0]; expect(copiedText).toContain('作品号:BF-001'); expect(copiedText).toContain('/runtime/big-fish?work=BF-001'); expect( screen.getByRole('button', { name: '分享内容已复制' }), ).toBeTruthy(); }); test('keeps moving in the last sampled direction after drag ends', async () => { 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, }); }); await waitFor(() => { expect(onSubmitInput).toHaveBeenCalledWith({ x: 1, y: 0 }); }); const callCountAfterDrag = onSubmitInput.mock.calls.length; act(() => { dispatchPointerEvent(stage, 'pointerup', { pointerId: 1, clientX: 140, clientY: 100, }); }); await waitFor(() => { expect(onSubmitInput.mock.calls.length).toBeGreaterThan(callCountAfterDrag); }); expect(onSubmitInput).toHaveBeenLastCalledWith({ x: 1, y: 0 }); }); });