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