b601b3b57e
新增 PlatformRuntimeStatusToast 统一运行态短错误、成功和反馈 toast 迁移跳一跳、拼图、敲木鱼、方洞和宝贝爱画运行态状态 chip 补充公共组件与运行态回归测试,并更新 PlatformUiKit 文档和 Hermes 决策记录
272 lines
8.8 KiB
TypeScript
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();
|
|
});
|