Files
Genarrative/packages/shared/src/components/ui.test.tsx
T
kdletters c5200c7d45
Project CI / Backend tests (push) Successful in 8m31s
Project CI / Repository checks (push) Successful in 14m14s
Project CI / Frontend tests (push) Successful in 13m2s
Project CI / Native shell tests (push) Successful in 21m30s
补齐共享 UI 组件库与展示页交互 (#202)
## 变更内容

- 新增基于 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>
2026-08-31 14:10:44 +08:00

210 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @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);
});
});