收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -0,0 +1,225 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import { PlatformUploadPreviewCard } from './PlatformUploadPreviewCard';
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: ({
src,
alt,
className,
}: {
src?: string | null;
alt?: string;
className?: string;
}) => <img src={src ?? ''} alt={alt} className={className} />,
}));
test('renders uploaded image preview with shared chrome', () => {
render(
<PlatformUploadPreviewCard
imageSrc="data:image/png;base64,abc"
imageAlt="反馈凭证预览"
removeLabel="移除上传凭证"
onRemove={vi.fn()}
/>,
);
const image = screen.getByRole('img', { name: '反馈凭证预览' });
const removeButton = screen.getByRole('button', { name: '移除上传凭证' });
expect(image.getAttribute('src')).toBe('data:image/png;base64,abc');
expect(image.className).toContain('object-cover');
expect(removeButton.getAttribute('type')).toBe('button');
expect(removeButton.className).toContain('bg-black/55');
});
test('calls remove handler from preview action', () => {
const onRemove = vi.fn();
render(
<PlatformUploadPreviewCard
imageSrc="/preview.png"
imageAlt="上传图片预览"
removeLabel="移除图片"
onRemove={onRemove}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '移除图片' }));
expect(onRemove).toHaveBeenCalledTimes(1);
});
test('supports preview action with resolved asset image', () => {
const onPreview = vi.fn();
render(
<PlatformUploadPreviewCard
imageSrc="/generated/reference.png"
imageAlt="参考图"
previewLabel="预览参考图 1"
removeLabel="移除参考图"
onPreview={onPreview}
resolveAsset
/>,
);
const previewButton = screen.getByRole('button', { name: '预览参考图 1' });
fireEvent.click(previewButton);
expect(screen.getByRole('img', { name: '参考图' }).getAttribute('src')).toBe(
'/generated/reference.png',
);
expect(onPreview).toHaveBeenCalledTimes(1);
});
test('can render preview-only cards without remove action', () => {
render(
<PlatformUploadPreviewCard
imageSrc="/preview.png"
imageAlt="上传图片预览"
removeLabel="移除图片"
/>,
);
expect(screen.getByRole('img', { name: '上传图片预览' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '移除图片' })).toBeNull();
});
test('supports captioned preview cards for reference images', () => {
const onPreview = vi.fn();
render(
<PlatformUploadPreviewCard
imageSrc="/reference.png"
imageAlt=""
caption="草莓"
previewLabel="预览参考图 草莓"
removeLabel="移除参考图 草莓"
onPreview={onPreview}
onRemove={vi.fn()}
className="w-full rounded-[1rem]"
imageShellClassName="ring-1"
captionClassName="pr-9"
/>,
);
const previewButton = screen.getByRole('button', {
name: '预览参考图 草莓',
});
fireEvent.click(previewButton);
const previewImage = previewButton.querySelector('img');
expect(screen.getByText('草莓').className).toContain('pr-9');
expect(previewImage?.parentElement?.className).toContain('ring-1');
expect(onPreview).toHaveBeenCalledTimes(1);
});
test('supports inline selected upload preview rows', () => {
const onRemove = vi.fn();
render(
<PlatformUploadPreviewCard
layout="inline"
imageSrc="/reference.png"
imageAlt="创作参考图"
caption="森林参考图.png"
removeLabel="移除参考图"
onRemove={onRemove}
className="mt-3"
removeButtonProps={{ title: '移除参考图' }}
/>,
);
const image = screen.getByRole('img', { name: '创作参考图' });
const row = image.closest('div')?.parentElement;
const removeButton = screen.getByRole('button', { name: '移除参考图' });
expect(row?.className).toContain('flex items-center gap-3');
expect(row?.className).toContain('bg-white/68');
expect(row?.className).toContain('px-3');
expect(row?.className).toContain('py-2');
expect(row?.className).toContain('mt-3');
expect(screen.getByText('森林参考图.png').className).toContain('truncate');
expect(removeButton.className).toContain('platform-icon-button');
expect(removeButton.getAttribute('title')).toBe('移除参考图');
fireEvent.click(removeButton);
expect(onRemove).toHaveBeenCalledTimes(1);
});
test('supports editor dark inline selected upload preview rows', () => {
render(
<PlatformUploadPreviewCard
layout="inline"
surface="editorDark"
imageSrc="/reference.png"
imageAlt="封面参考图"
caption="已载入封面参考图"
removeLabel="移除封面参考图"
onRemove={vi.fn()}
imageShellClassName="!h-16 !w-24 rounded-xl"
/>,
);
const image = screen.getByRole('img', { name: '封面参考图' });
const imageShell = image.parentElement;
const row = imageShell?.parentElement;
const caption = screen.getByText('已载入封面参考图');
const removeButton = screen.getByRole('button', { name: '移除封面参考图' });
expect(row?.className).toContain('border-white/10');
expect(row?.className).toContain('bg-black/25');
expect(imageShell?.className).toContain('bg-black/30');
expect(imageShell?.className).toContain('!w-24');
expect(caption.className).toContain('text-zinc-300');
expect(removeButton.className).toContain('bg-black/55');
});
test('keeps disabled remove action inert', () => {
const onRemove = vi.fn();
render(
<PlatformUploadPreviewCard
imageSrc="/preview.png"
imageAlt="上传图片预览"
removeLabel="移除图片"
onRemove={onRemove}
disabled
/>,
);
const removeButton = screen.getByRole('button', { name: '移除图片' });
fireEvent.click(removeButton);
expect(removeButton).toHaveProperty('disabled', true);
expect(removeButton.className).toContain('disabled:cursor-not-allowed');
expect(onRemove).not.toHaveBeenCalled();
});
test('supports local size, image and remove button classes', () => {
render(
<PlatformUploadPreviewCard
imageSrc="/preview.png"
imageAlt="上传图片预览"
removeLabel="移除图片"
onRemove={vi.fn()}
className="h-20 w-20"
imageClassName="object-top"
removeButtonProps={{ className: 'right-2 top-2' }}
/>,
);
const image = screen.getByRole('img', { name: '上传图片预览' });
const removeButton = screen.getByRole('button', { name: '移除图片' });
expect(image.parentElement?.className).toContain('h-20');
expect(image.className).toContain('object-top');
expect(removeButton.className).toContain('right-2');
});