1ad25e30f8
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
296 lines
8.3 KiB
TypeScript
296 lines
8.3 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import { createRef, type ImgHTMLAttributes } from 'react';
|
|
import { expect, test, vi } from 'vitest';
|
|
|
|
import { PlatformMediaFrame } from './PlatformMediaFrame';
|
|
|
|
vi.mock('../ResolvedAssetImage', () => ({
|
|
ResolvedAssetImage: ({
|
|
src,
|
|
fallbackSrc,
|
|
alt,
|
|
className,
|
|
refreshKey,
|
|
...imageProps
|
|
}: {
|
|
src?: string;
|
|
fallbackSrc?: string;
|
|
alt: string;
|
|
className?: string;
|
|
refreshKey?: string | number | null;
|
|
} & ImgHTMLAttributes<HTMLImageElement>) => (
|
|
<img
|
|
{...imageProps}
|
|
src={src ?? fallbackSrc}
|
|
data-fallback-src={fallbackSrc}
|
|
data-refresh-key={refreshKey ?? undefined}
|
|
alt={alt}
|
|
className={className}
|
|
/>
|
|
),
|
|
}));
|
|
|
|
test('renders warm media frame with image and landscape aspect', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/scene.png"
|
|
alt="潮灯居"
|
|
fallbackLabel="场景"
|
|
aspect="landscape"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '潮灯居' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('platform-media-frame');
|
|
expect(frame?.className).toContain('aspect-[16/9]');
|
|
expect(frame?.className).toContain(
|
|
'border-[var(--platform-subpanel-border)]',
|
|
);
|
|
expect(frame?.className).toContain('radial-gradient');
|
|
expect(image.className).toContain('object-cover');
|
|
});
|
|
|
|
test('renders fallback label when no image source is available', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
alt="角色"
|
|
fallbackLabel="角色"
|
|
aspect="square"
|
|
surface="plain"
|
|
/>,
|
|
);
|
|
|
|
const fallback = screen.getByText('角色');
|
|
const frame = fallback.closest('div.relative');
|
|
|
|
expect(frame?.className).toContain('aspect-square');
|
|
expect(frame?.className).toContain('bg-[var(--platform-subpanel-fill)]');
|
|
expect(fallback.className).toContain('tracking-[0.18em]');
|
|
});
|
|
|
|
test('supports custom fallback content', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
alt="封面图"
|
|
fallbackLabel="封面图占位"
|
|
fallbackContent={<span data-testid="fallback-icon">图标</span>}
|
|
aspect="standard"
|
|
surface="plain"
|
|
fallbackShellClassName="bg-rainbow"
|
|
fallbackClassName="tracking-normal"
|
|
/>,
|
|
);
|
|
|
|
const fallbackIcon = screen.getByTestId('fallback-icon');
|
|
const fallback = fallbackIcon.closest('div');
|
|
const frame = fallbackIcon.closest('div.relative');
|
|
|
|
expect(fallbackIcon.textContent).toBe('图标');
|
|
expect(screen.queryByText('封面图占位')).toBeNull();
|
|
expect(fallback?.className).toContain('bg-rainbow');
|
|
expect(fallback?.className).toContain('tracking-normal');
|
|
expect(frame?.className).toContain('aspect-[4/3]');
|
|
});
|
|
|
|
test('supports soft media frame surface', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/template-preview.webp"
|
|
alt="创意模板"
|
|
fallbackLabel="创意模板"
|
|
aspect="landscape"
|
|
surface="soft"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '创意模板' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-[16/9]');
|
|
expect(frame?.className).toContain(
|
|
'border-[var(--platform-subpanel-border)]',
|
|
);
|
|
expect(frame?.className).toContain('bg-white/68');
|
|
});
|
|
|
|
test('supports bright media frame surface', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/match3d/item.png"
|
|
alt="物品素材"
|
|
fallbackLabel="物品素材"
|
|
aspect="square"
|
|
surface="bright"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '物品素材' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-square');
|
|
expect(frame?.className).toContain(
|
|
'border-[var(--platform-subpanel-border)]',
|
|
);
|
|
expect(frame?.className).toContain('bg-white/82');
|
|
});
|
|
|
|
test('passes div attributes to the media frame container', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/match3d/item.png"
|
|
alt="物品素材"
|
|
fallbackLabel="物品素材"
|
|
surface="bright"
|
|
data-testid="match3d-preview-frame"
|
|
aria-label="物品素材预览"
|
|
/>,
|
|
);
|
|
|
|
const frame = screen.getByTestId('match3d-preview-frame');
|
|
|
|
expect(frame.getAttribute('aria-label')).toBe('物品素材预览');
|
|
expect(frame.className).toContain('bg-white/82');
|
|
});
|
|
|
|
test('supports none surface for nested interactive shells', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/match3d/thumb.png"
|
|
alt="缩略图"
|
|
fallbackLabel="缩略图"
|
|
aspect="square"
|
|
surface="none"
|
|
className="rounded-[0.65rem]"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '缩略图' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-square');
|
|
expect(frame?.className).toContain('rounded-[0.65rem]');
|
|
expect(frame?.className).not.toContain('border-[');
|
|
expect(frame?.className).not.toContain('bg-white');
|
|
});
|
|
|
|
test('supports editor dark surface, fallback source and overlay', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src=""
|
|
fallbackSrc="/fallback.png"
|
|
alt="第1幕"
|
|
fallbackLabel="第1幕"
|
|
surface="editorDark"
|
|
loading="lazy"
|
|
overlayInteractive
|
|
previewOverlay={<span>覆盖层</span>}
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '第1幕' });
|
|
const frame = image.closest('div');
|
|
const overlay = screen.getByText('覆盖层').parentElement;
|
|
|
|
expect(image.getAttribute('src')).toBe('/fallback.png');
|
|
expect(frame?.className).toContain('border-white/10');
|
|
expect(frame?.className).toContain('rgba(19,24,39,0.95)');
|
|
expect(overlay?.className).toContain('pointer-events-auto');
|
|
});
|
|
|
|
test('supports standard aspect, bare surface and refresh key', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/puzzle/level.png"
|
|
alt="雨夜猫街"
|
|
fallbackLabel="暂无正式图"
|
|
aspect="standard"
|
|
surface="bare"
|
|
refreshKey="session-1:level-1"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '雨夜猫街' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-[4/3]');
|
|
expect(frame?.className).toContain('bg-[var(--platform-subpanel-fill)]');
|
|
expect(frame?.className).not.toContain('border');
|
|
expect(image.getAttribute('data-refresh-key')).toBe('session-1:level-1');
|
|
});
|
|
|
|
test('supports portrait aspect for vertical preview assets', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/puzzle-clear/board-background.png"
|
|
alt="场地底图"
|
|
fallbackLabel="底图"
|
|
aspect="portrait"
|
|
surface="bare"
|
|
className="rounded-none bg-white/80"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '场地底图' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-[9/16]');
|
|
expect(frame?.className).toContain('bg-white/80');
|
|
expect(frame?.className).toContain('rounded-none');
|
|
});
|
|
|
|
test('supports wide aspect for broad preview assets', () => {
|
|
render(
|
|
<PlatformMediaFrame
|
|
src="/big-fish/asset-preview.png"
|
|
alt="大鱼素材候选"
|
|
fallbackLabel="AI 资产候选预览"
|
|
aspect="wide"
|
|
surface="plain"
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByRole('img', { name: '大鱼素材候选' });
|
|
const frame = image.closest('div');
|
|
|
|
expect(frame?.className).toContain('aspect-[9/5]');
|
|
expect(frame?.className).toContain(
|
|
'border-[var(--platform-subpanel-border)]',
|
|
);
|
|
});
|
|
|
|
test('supports auto aspect, image props and container refs', () => {
|
|
const frameRef = createRef<HTMLDivElement>();
|
|
|
|
render(
|
|
<PlatformMediaFrame
|
|
ref={frameRef}
|
|
src="/cover-upload.png"
|
|
alt="封面裁剪预览"
|
|
fallbackLabel="封面"
|
|
aspect="auto"
|
|
surface="none"
|
|
imageClassName="absolute max-w-none object-fill"
|
|
imageProps={{
|
|
draggable: false,
|
|
style: { left: '-10%', width: '120%' },
|
|
}}
|
|
data-testid="cover-crop-frame"
|
|
/>,
|
|
);
|
|
|
|
const frame = screen.getByTestId('cover-crop-frame');
|
|
const image = screen.getByRole('img', { name: '封面裁剪预览' });
|
|
|
|
expect(frameRef.current).toBe(frame);
|
|
expect(frame.className).toContain('platform-media-frame');
|
|
expect(frame.className).not.toContain('aspect-[');
|
|
expect(frame.className).not.toContain('aspect-square');
|
|
expect(image.getAttribute('draggable')).toBe('false');
|
|
expect(image.getAttribute('style')).toContain('left: -10%');
|
|
expect(image.className).toContain('absolute max-w-none object-fill');
|
|
});
|