新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
197 lines
5.4 KiB
TypeScript
197 lines
5.4 KiB
TypeScript
// @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 ? <img src={src} alt={alt} className={className} /> : 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(
|
||
<BigFishRuntimeShell
|
||
run={createRun('failed')}
|
||
onBack={onBack}
|
||
onRestart={onRestart}
|
||
onSubmitInput={() => {}}
|
||
/>,
|
||
);
|
||
|
||
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(
|
||
<BigFishRuntimeShell
|
||
run={createRun('won')}
|
||
onBack={onBack}
|
||
onSubmitInput={() => {}}
|
||
/>,
|
||
);
|
||
|
||
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(
|
||
<BigFishRuntimeShell
|
||
run={createRun('running')}
|
||
onBack={() => {}}
|
||
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(
|
||
<BigFishRuntimeShell
|
||
run={createRun('running')}
|
||
shareTitle="深海追击"
|
||
sharePublicWorkCode="BF-001"
|
||
onBack={() => {}}
|
||
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(
|
||
<BigFishRuntimeShell
|
||
run={createRun('running')}
|
||
onBack={() => {}}
|
||
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 });
|
||
});
|
||
});
|