frontend/test/card.test.ts
Rama master · réplica de lectura del repositorio original.
/* 共用卡面。
*
* 钉的是「卡片是一块填充面」这件事本身:Board 的卡从来不是描边空心壳,迁移时每页各拼一串
* `rounded-2xl border border-separator-border`,十个文件二十处,整个管理区掉了一层底色。
* 所以这里除了变体,还扫一遍 React 子树,拦住下一个自己拼卡面的人。 */
import { readdirSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { expect, it } from 'vitest';
import { cardClass } from '../src/react/components/card';
const classes = (options?: Parameters<typeof cardClass>[0]) =>
new Set(cardClass(options).split(' '));
it('默认那一档卡是填充的,没有描边', () => {
const card = classes();
expect(card).toContain('bg-background-secondary-default');
expect(card).toContain('shadow-card');
expect([...card].filter((name) => name.startsWith('border'))).toEqual([]);
});
it('圆角与内边距按变体给', () => {
expect(classes()).toContain('rounded-2xl');
expect(classes()).toContain('p-5');
// 图表卡大一档,和旧 `.board-radial-card{border-radius:20px}` 一致。
expect(classes({ radius: 'chart' })).toContain('rounded-2-5xl');
// 头、体、脚各自排内边距的卡(读数卡、分区)自己不留。
expect(classes({ padding: 'none' })).not.toContain('p-5');
});
it('描边卡是 primary 底加一条发丝线,不是透明空心壳', () => {
const outlined = classes({ variant: 'outlined' });
expect(outlined).toContain('bg-background-primary-default');
expect(outlined).toContain('border-separator-border');
expect(outlined).not.toContain('bg-background-secondary-default');
});
/* 旧 `.reviewitem` 收的是 `--surface-radius` 那一档,也没有接触阴影:阴影是给填充卡
「浮在次级底上」用的,描边卡本来就靠线收边,再压一层影子等于两套收边办法叠着用。 */
it('描边卡自己带 14px 圆角,不跟填充卡那一档走,也不压阴影', () => {
const outlined = classes({ variant: 'outlined' });
expect(outlined).toContain('rounded-surface');
expect(outlined).not.toContain('shadow-card');
// 点名要别的圆角时仍然听调用方的。
expect(classes({ variant: 'outlined', radius: 'chart' })).toContain('rounded-2-5xl');
});
it('卡里再浮一层是 primary 底、不画线', () => {
const raised = classes({ variant: 'raised' });
expect(raised).toContain('bg-background-primary-default');
expect([...raised].filter((name) => name.startsWith('border'))).toEqual([]);
});
it('填充卡之上另加的那条描边不换底', () => {
const fieldset = classes({ bordered: 'line' });
expect(fieldset).toContain('bg-background-secondary-default');
expect(fieldset).toContain('border-border-button-default');
expect(classes({ bordered: 'soft' })).toContain('border-separator-border');
// 失败卡的框线要有宽度才画得出来,且画成不占盒子的 outline,不把内容挤进 1px。
const failed = [...classes({ bordered: 'error' })];
expect(failed.filter((name) => name.startsWith('border'))).toEqual([]);
expect(failed).toEqual(expect.arrayContaining(['outline-1', '-outline-offset-1', 'outline-border-error-default']));
});
/* 环画在 `::after` 覆盖层上:inset 的 box-shadow 画在子元素底下,脚注带一铺底色就盖掉环的下半截。 */
it('选中态在最上层压一圈 2px 描边,收掉接触阴影,悬停不盖选中', () => {
const tab = cardClass({ interactive: true, selectable: true });
expect(tab).toContain('relative');
expect(tab).toContain('data-selected:shadow-none');
expect(tab).toContain('data-selected:after:absolute');
expect(tab).toContain('data-selected:after:inset-0');
expect(tab).toContain('data-selected:after:z-1');
expect(tab).toContain('data-selected:after:rounded-[inherit]');
expect(tab).toContain('data-selected:after:border-2');
expect(tab).toContain('data-selected:after:border-border-focus-ring');
expect(tab).not.toContain('ring-inset');
// 悬停那条写成 `not-data-selected:`,不指望工具类的先后顺序。
expect(tab).toContain('not-data-selected:hover:bg-card-hover');
expect(tab).not.toContain(' hover:bg-card-hover');
});
/** React 子树里除 BoardUI 源码外的 `.tsx`。 */
function sources(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = resolve(dir, entry.name);
if (entry.isDirectory()) return entry.name === 'boardui' ? [] : sources(path);
return entry.name.endsWith('.tsx') ? [path] : [];
});
}
it('页面不自己拼卡面', () => {
const offenders = sources(resolve(process.cwd(), 'src/react'))
.filter((path) => {
const code = readFileSync(path, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
// 描边配上卡片那两档圆角(16px、20px)就是一张卡,卡面走 `components/card.tsx`。
// 剩下的 `border-t` / `border-b` / `border-y` 是分隔线,`rounded-2lg` 是卡里的小块,
// `shadow-dropdown` 是浮层——浮层本来就描边。
return /className=\{?"[^"]*\bborder border-/.test(code)
&& /className=\{?"[^"]*\brounded-(?:2xl|2-5xl)\b/.test(code)
&& !code.includes('shadow-dropdown');
})
.map((path) => path.split(/[\\/]/).slice(-2).join('/'));
expect(offenders).toEqual([]);
});