Files
Genarrative/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx
T
kdletters b601b3b57e 收口运行态状态提示组件
新增 PlatformRuntimeStatusToast 统一运行态短错误、成功和反馈 toast
迁移跳一跳、拼图、敲木鱼、方洞和宝贝爱画运行态状态 chip
补充公共组件与运行态回归测试,并更新 PlatformUiKit 文档和 Hermes 决策记录
2026-06-10 11:24:40 +08:00

272 lines
8.8 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { WoodenFishWorkProfileResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { WoodenFishRuntimeShell } from './WoodenFishRuntimeShell';
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('../../hooks/useResolvedAssetReadUrl', () => ({
useResolvedAssetReadUrl: (src?: string | null) => ({
resolvedUrl: src?.trim() ?? '',
isResolving: false,
shouldResolve: false,
}),
}));
let audioConstructor: ReturnType<typeof vi.fn>;
function createProfile(): WoodenFishWorkProfileResponse {
return {
summary: {
runtimeKind: 'wooden-fish',
workId: 'wooden-fish-work-1',
profileId: 'wooden-fish-profile-1',
ownerUserId: 'user-1',
sourceSessionId: null,
workTitle: '今日敲木鱼',
workDescription: '',
themeTags: ['敲木鱼'],
coverImageSrc: null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-22T00:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'ready',
},
draft: {
templateId: 'wooden-fish',
templateName: '敲木鱼',
profileId: 'wooden-fish-profile-1',
workTitle: '今日敲木鱼',
workDescription: '',
themeTags: ['敲木鱼'],
hitObjectPrompt: '金色木鱼',
hitObjectReferenceImageSrc: null,
hitSoundPrompt: null,
floatingWords: ['幸运', '功德'],
hitObjectAsset: {
assetId: 'wooden-fish-hit-object-1',
imageSrc: '/wooden-fish/default-hit-object.png',
imageObjectKey: 'public/wooden-fish/default-hit-object.png',
assetObjectId: 'wooden-fish-hit-object-asset',
generationProvider: 'bundled-default',
prompt: '默认敲击物图案,圆润木质质感,透明背景',
width: 1024,
height: 1024,
},
backgroundAsset: null,
backButtonAsset: {
assetId: 'wooden-fish-back-button-1',
imageSrc: '/generated/back-button.png',
imageObjectKey: 'generated/back-button.png',
assetObjectId: 'wooden-fish-back-button-asset',
generationProvider: 'image2',
prompt: '主题返回按钮',
width: 1024,
height: 1024,
},
hitSoundAsset: null,
coverImageSrc: null,
generationStatus: 'ready',
},
hitObjectAsset: {
assetId: 'wooden-fish-hit-object-1',
imageSrc: '/wooden-fish/default-hit-object.png',
imageObjectKey: 'public/wooden-fish/default-hit-object.png',
assetObjectId: 'wooden-fish-hit-object-asset',
generationProvider: 'bundled-default',
prompt: '默认敲击物图案,圆润木质质感,透明背景',
width: 1024,
height: 1024,
},
backgroundAsset: null,
backButtonAsset: {
assetId: 'wooden-fish-back-button-1',
imageSrc: '/generated/back-button.png',
imageObjectKey: 'generated/back-button.png',
assetObjectId: 'wooden-fish-back-button-asset',
generationProvider: 'image2',
prompt: '主题返回按钮',
width: 1024,
height: 1024,
},
hitSoundAsset: null as unknown as WoodenFishWorkProfileResponse['hitSoundAsset'],
floatingWords: ['幸运', '功德'],
};
}
function createRun() {
return {
runId: 'wooden-fish-run-1',
profileId: 'wooden-fish-profile-1',
ownerUserId: 'user-1',
status: 'playing' as const,
totalTapCount: 284,
wordCounters: [
{ text: '幸运', count: 63 },
{ text: '功德', count: 17 },
],
startedAtMs: 1_717_000_000_000,
updatedAtMs: 1_717_000_006_000,
finishedAtMs: null,
};
}
afterEach(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
audioConstructor = vi.fn().mockImplementation((src: string) => ({
src,
preload: '',
pause: vi.fn(),
play: vi.fn().mockResolvedValue(undefined),
currentTime: 0,
}));
vi.stubGlobal('Audio', audioConstructor);
});
test('运行态缺少音频资产时使用默认木鱼音', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} />);
expect(audioConstructor).toHaveBeenCalledWith(
'/wooden-fish/default-hit-sound.mp3',
);
});
test('运行态为敲击音效预创建 10 路复音池', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} />);
expect(audioConstructor).toHaveBeenCalledTimes(10);
});
test('顶部只展示总数,点击后展开子项计数器面板,点外部收起', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />);
const totalCounterButton = screen.getByRole('button', { name: /总数 284/ });
expect(totalCounterButton).toBeTruthy();
expect(screen.queryByTestId('wooden-fish-counter-panel')).toBeNull();
expect(screen.queryByText('幸运')).toBeNull();
expect(screen.queryByText('功德')).toBeNull();
fireEvent.click(totalCounterButton);
const panel = screen.getByTestId('wooden-fish-counter-panel');
const panelRows = within(panel).getAllByText(/幸运|功德/);
expect(panelRows).toHaveLength(2);
expect(within(panel).getByText('63')).toBeTruthy();
expect(within(panel).getByText('17')).toBeTruthy();
fireEvent.pointerDown(document.body);
expect(screen.queryByTestId('wooden-fish-counter-panel')).toBeNull();
});
test('木鱼运行态不渲染右上角重开按钮', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />);
expect(screen.queryByRole('button', { name: '重开' })).toBeNull();
});
test('木鱼运行态使用生成的主题返回按钮图', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />);
const backButton = screen.getByRole('button', { name: '返回' });
const image = backButton.querySelector('img');
expect(image?.getAttribute('src')).toBe('/generated/back-button.png');
expect(backButton.className).not.toContain('backdrop-blur');
expect(backButton.className).not.toContain('bg-white');
expect(backButton.className).toContain('h-10');
expect(backButton.className).toContain('w-10');
});
test('木鱼运行态错误提示使用公共运行态 toast', () => {
render(
<WoodenFishRuntimeShell
profile={createProfile()}
run={createRun()}
error="同步失败"
/>,
);
const toast = screen.getByRole('alert');
expect(toast.textContent).toBe('同步失败');
expect(toast.className).toContain('platform-runtime-status-toast');
expect(toast.getAttribute('data-wooden-fish-functional')).toBe('true');
});
test('木鱼运行态飘字去掉底板并放大字号', () => {
const { container } = render(
<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />,
);
const playfield = container.querySelector('.wooden-fish-runtime');
expect(playfield).not.toBeNull();
if (!playfield) {
throw new Error('缺少木鱼运行态根节点。');
}
fireEvent.pointerDown(playfield, { clientX: 260, clientY: 360 });
const floatingText = screen.getByText(/幸运|功德/);
expect(floatingText.className).toContain('text-[1.75rem]');
expect(floatingText.className).not.toContain('bg-slate-950/78');
expect(floatingText.className).not.toContain('rounded-full');
});
test('子项计数器面板预置全部词条,未出现词条初始值为0', () => {
render(
<WoodenFishRuntimeShell
profile={createProfile()}
run={{
...createRun(),
totalTapCount: 63,
wordCounters: [{ text: '幸运', count: 63 }],
}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: /总数 63/ }));
const panel = screen.getByTestId('wooden-fish-counter-panel');
expect(within(panel).getByText('幸运')).toBeTruthy();
expect(within(panel).getByText('63')).toBeTruthy();
expect(within(panel).getByText('功德')).toBeTruthy();
expect(within(panel).getByText('0')).toBeTruthy();
});
test('顶部总数条的 logo 使用紧凑标题条布局', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />);
const totalCounterButton = screen.getByRole('button', { name: /总数 284/ });
const logo = totalCounterButton.querySelector(
'[data-testid="wooden-fish-runtime-logo"]',
) as HTMLImageElement | null;
expect(logo).toBeTruthy();
expect(logo?.className).toContain('wooden-fish-runtime__counter-logo__image');
expect(logo?.tagName).toBe('IMG');
});
test('木鱼运行态不渲染底部进行中和结束操作区', () => {
render(<WoodenFishRuntimeShell profile={createProfile()} run={createRun()} />);
expect(screen.queryByText('进行中')).toBeNull();
expect(screen.queryByRole('button', { name: '结束' })).toBeNull();
});