c5200c7d45
## 变更内容 - 新增基于 shadcn open-code 模式的共享 UI canonical 组件、样式与导出。 - 新增 `/components` 共享组件展示页,并覆盖平台组件、Token、状态和移动端布局。 - 补齐平台展示页的筛选、排序、上传预览、标签、开关和异步状态交互。 - 修复排序导致预览主题变化、筛选弹窗误关闭和入口状态不更新的问题。 - 同步网站与客户端构建别名、依赖、路由测试和项目文档。 ## 验证 - `npm run test -- src/components/shared-components/SharedComponentsShowcasePage.test.tsx` - `npm run typecheck` - `npm run check:encoding` - `npm run build:raw` - `git diff --check` - pre-push 门禁通过 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/202 Co-authored-by: kdletters <kdletters@qq.com> Co-committed-by: kdletters <kdletters@qq.com>
210 lines
6.1 KiB
TypeScript
210 lines
6.1 KiB
TypeScript
/* @vitest-environment jsdom */
|
||
|
||
import { fireEvent, render, screen } from '@testing-library/react';
|
||
import { describe, expect, test, vi } from 'vitest';
|
||
|
||
import {
|
||
Badge,
|
||
Button,
|
||
Checkbox,
|
||
EmptyState,
|
||
IconButton,
|
||
Label,
|
||
Modal,
|
||
PlatformActionButton,
|
||
PlatformIconButton,
|
||
ProgressBar,
|
||
SegmentedTabs,
|
||
SelectField,
|
||
Skeleton,
|
||
Status,
|
||
Subpanel,
|
||
Switch,
|
||
Table,
|
||
TableBody,
|
||
TableCaption,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
TextField,
|
||
} from './index';
|
||
|
||
describe('shared UI components', () => {
|
||
test('keeps native button semantics and states', () => {
|
||
const onClick = vi.fn();
|
||
render(
|
||
<div className="genarrative-ui">
|
||
<Button onClick={onClick}>保存</Button>
|
||
<Button loading>稍等</Button>
|
||
<IconButton label="关闭" icon="×" />
|
||
</div>,
|
||
);
|
||
|
||
const button = screen.getByRole('button', { name: '保存' });
|
||
expect(button.getAttribute('type')).toBe('button');
|
||
fireEvent.click(button);
|
||
expect(onClick).toHaveBeenCalledTimes(1);
|
||
expect(
|
||
(screen.getByRole('button', { name: '稍等' }) as HTMLButtonElement)
|
||
.disabled,
|
||
).toBe(true);
|
||
expect(
|
||
screen.getByRole('button', { name: '关闭' }).getAttribute('title'),
|
||
).toBe('关闭');
|
||
});
|
||
|
||
test('connects field labels and descriptions to native controls', () => {
|
||
render(
|
||
<div className="genarrative-ui">
|
||
<TextField label="名称" hint="最多 20 字" error="名称已存在" />
|
||
<TextField label="描述" multiline />
|
||
<SelectField label="主题" hint="选择颜色">
|
||
<option value="warm">暖色</option>
|
||
</SelectField>
|
||
</div>,
|
||
);
|
||
|
||
const input = screen.getByLabelText('名称');
|
||
expect(input.getAttribute('aria-invalid')).toBe('true');
|
||
expect(input.getAttribute('aria-describedby')).toContain('hint');
|
||
expect(screen.getByLabelText('描述').tagName).toBe('TEXTAREA');
|
||
expect(screen.getByLabelText('主题').tagName).toBe('SELECT');
|
||
expect(screen.getByRole('alert').textContent).toContain('名称已存在');
|
||
});
|
||
|
||
test('renders slots, selected tabs and semantic status content', () => {
|
||
const onChange = vi.fn();
|
||
render(
|
||
<div className="genarrative-ui">
|
||
<Subpanel title="设置" actions={<Badge>稳定</Badge>}>
|
||
内容
|
||
</Subpanel>
|
||
<Status tone="success" icon="✓">
|
||
完成
|
||
</Status>
|
||
<EmptyState title="暂无内容" />
|
||
<ProgressBar value={140} label="进度" />
|
||
<SegmentedTabs
|
||
items={[
|
||
{ id: 'a', label: 'A' },
|
||
{ id: 'b', label: 'B' },
|
||
]}
|
||
activeId="a"
|
||
onChange={onChange}
|
||
label="分区"
|
||
/>
|
||
<Switch checked label="启用" />
|
||
</div>,
|
||
);
|
||
|
||
expect(screen.getByRole('heading', { name: '设置' })).toBeTruthy();
|
||
expect(screen.getByRole('status').textContent).toContain('完成');
|
||
expect(screen.getByRole('progressbar').getAttribute('aria-valuenow')).toBe(
|
||
'100',
|
||
);
|
||
expect(
|
||
screen.getByRole('tab', { name: 'A' }).getAttribute('aria-selected'),
|
||
).toBe('true');
|
||
fireEvent.click(screen.getByRole('tab', { name: 'B' }));
|
||
expect(onChange).toHaveBeenCalledWith('b');
|
||
expect(
|
||
screen.getByRole('switch', { name: '启用' }).getAttribute('aria-checked'),
|
||
).toBe('true');
|
||
});
|
||
|
||
test('provides semantic label, checkbox, skeleton and table primitives', () => {
|
||
render(
|
||
<div className="genarrative-ui">
|
||
<div>
|
||
<Checkbox id="autosave" defaultChecked />
|
||
<Label htmlFor="autosave">自动保存</Label>
|
||
</div>
|
||
<Skeleton aria-label="读取中" className="h-3 w-20" />
|
||
<Table>
|
||
<TableCaption>最近项目</TableCaption>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>名称</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
<TableRow>
|
||
<TableCell>珊瑚岛</TableCell>
|
||
<TableCell>已发布</TableCell>
|
||
</TableRow>
|
||
</TableBody>
|
||
</Table>
|
||
</div>,
|
||
);
|
||
|
||
expect(screen.getByLabelText('自动保存')).toHaveProperty('checked', true);
|
||
expect(screen.getByLabelText('读取中').getAttribute('data-slot')).toBe(
|
||
'skeleton',
|
||
);
|
||
expect(screen.getByRole('table', { name: '最近项目' })).toBeTruthy();
|
||
expect(screen.getByRole('cell', { name: '珊瑚岛' })).toBeTruthy();
|
||
});
|
||
|
||
test('closes modal on escape and backdrop click', () => {
|
||
const onClose = vi.fn();
|
||
render(
|
||
<Modal open title="确认" onClose={onClose}>
|
||
正文
|
||
</Modal>,
|
||
);
|
||
|
||
expect(screen.getByRole('dialog', { name: '确认' })).toBeTruthy();
|
||
fireEvent.keyDown(document, { key: 'Escape' });
|
||
expect(onClose).toHaveBeenCalledTimes(1);
|
||
fireEvent.mouseDown(
|
||
document.querySelector('.genarrative-ui-modal__backdrop')!,
|
||
);
|
||
expect(onClose).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
test('can keep modal open when backdrop and escape closing are disabled', () => {
|
||
const onClose = vi.fn();
|
||
render(
|
||
<Modal
|
||
open
|
||
title="锁定"
|
||
onClose={onClose}
|
||
closeOnBackdrop={false}
|
||
closeOnEscape={false}
|
||
>
|
||
正文
|
||
</Modal>,
|
||
);
|
||
|
||
fireEvent.keyDown(document, { key: 'Escape' });
|
||
fireEvent.mouseDown(
|
||
document.querySelector('.genarrative-ui-modal__backdrop')!,
|
||
);
|
||
expect(onClose).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('keeps platform wrappers generic and supports label/span hosts', () => {
|
||
const onClick = vi.fn();
|
||
render(
|
||
<div className="genarrative-ui">
|
||
<PlatformActionButton asChild="label" htmlFor="upload" tone="accent">
|
||
上传
|
||
</PlatformActionButton>
|
||
<PlatformIconButton
|
||
asChild="spanButton"
|
||
label="打开"
|
||
icon="+"
|
||
onClick={onClick}
|
||
/>
|
||
</div>,
|
||
);
|
||
|
||
expect(screen.getByText('上传').closest('label')?.htmlFor).toBe('upload');
|
||
const spanButton = screen.getByRole('button', { name: '打开' });
|
||
fireEvent.keyDown(spanButton, { key: 'Enter' });
|
||
expect(onClick).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|