Files
lingxi/apps/web/src/api/dicts.spec.ts
T
Stev_Wang 3b0a99d91f feat(web): A4 数据字典与系统配置页
背景:用户选定方案 A 主从分栏,A4 需要落地字典与配置页。\n改动:新增 /system/dicts、/system/dicts/:type、/system/settings;主从类型列表 + 三层合并 + 树/词表切换 + 条目/类型弹层;6 组配置 + 命名预览 + 阈值二次确认 + 变更记录;客户字段上限改读 effective config;路由/样式/OpenAPI 类型同步。\n影响面:管理中心新增两个菜单页;客户页上限显示改为配置读取(fallback 共享常量)。\n回滚:回滚本提交并恢复 AppLayout / routes / main 样式 import。\n验证:web typecheck/lint/build;单测 154;浏览器自检 1440/1024 无溢出。
2026-11-04 11:49:32 +08:00

210 lines
6.3 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.
import type {
DictItem,
DictItemListResponse,
DictTypeDetail,
DictTypeSummary,
} from '@lingxi/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/stores/auth-store';
import { setUnauthorizedHandler } from './client';
import {
createDictItem,
createDictType,
deleteDictType,
fetchDictItemList,
fetchDictTypeDetail,
fetchDictTypeList,
setDictTypeStatus,
updateDictItem,
updateDictType,
} from './dicts';
const NOW = '2026-11-04T10:00:00.000Z';
const ID = '01HZZZZZZZZZZZZZZZZZZZZZZ1';
const jsonResponse = (body: unknown, status = 200): Response =>
new Response(status === 204 ? null : JSON.stringify(body), {
status,
headers: status === 204 ? undefined : { 'Content-Type': 'application/json' },
});
const typeSummary: DictTypeSummary = {
code: 'script_direction',
name: '脚本方向',
group: 'content',
kind: 'list',
description: '脚本“说什么”的方向',
industryScoped: true,
builtin: true,
status: 'active',
sortOrder: 60,
itemCount: 11,
activeItemCount: 11,
industryCount: 4,
updatedAt: NOW,
updatedBy: '江岚',
};
const item: DictItem = {
id: ID,
typeCode: 'script_direction',
code: 'oral',
name: '口播',
parentCode: null,
parentName: null,
industryId: null,
industryPath: null,
sortOrder: 10,
status: 'active',
remark: '',
source: 'generic',
overridden: false,
refCount: 15,
updatedAt: NOW,
updatedBy: '江岚',
};
const typeDetail: DictTypeDetail = { ...typeSummary, items: [item] };
const itemList: DictItemListResponse = {
items: [item],
next_cursor: null,
layers: [
{ source: 'generic', industryId: null, label: '通用', count: 5 },
{ source: 'parent', industryId: ID, label: '父行业', count: 2 },
{ source: 'current', industryId: ID, label: '本行业', count: 1 },
],
effectiveCount: 7,
overriddenCount: 1,
disabledCount: 0,
};
function lastCall(): { url: string; init: RequestInit | undefined } {
const fetchMock = vi.mocked(globalThis.fetch);
const call = fetchMock.mock.calls.at(-1);
const input = call?.[0];
const url =
typeof input === 'string' ? input : input instanceof URL ? input.href : (input?.url ?? '');
return { url, init: call?.[1] };
}
function lastBody(): unknown {
const body = lastCall().init?.body;
return typeof body === 'string' ? (JSON.parse(body) as unknown) : undefined;
}
beforeEach(() => {
vi.restoreAllMocks();
window.localStorage.clear();
useAuthStore.getState().clearSession();
setUnauthorizedHandler(null);
useAuthStore.getState().setSession({
accessToken: 'token',
user: { id: ID, name: '江岚', account: 'jiang.lan' },
permissions: ['dict:manage'],
});
});
describe('数据字典接口封装', () => {
it('类型列表:query 用 snake_case(keyword / group / status / limit / cursor)', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
jsonResponse({ items: [typeSummary], next_cursor: 'cursor-2' }),
);
const result = await fetchDictTypeList({
keyword: '脚本',
group: 'content',
status: 'active',
limit: 100,
cursor: 'cursor-1',
});
expect(result.next_cursor).toBe('cursor-2');
expect(lastCall().url).toContain('/dicts?keyword=');
expect(lastCall().url).toContain('group=content');
expect(lastCall().url).toContain('status=active');
expect(lastCall().url).toContain('limit=100');
expect(lastCall().url).toContain('cursor=cursor-1');
});
it('类型详情 / 新建 / 更新 / 状态 / 删除', async () => {
vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(jsonResponse(typeDetail))
.mockResolvedValueOnce(jsonResponse(typeDetail, 201))
.mockResolvedValueOnce(jsonResponse(typeDetail))
.mockResolvedValueOnce(jsonResponse(typeDetail))
.mockResolvedValueOnce(jsonResponse(undefined, 204));
await fetchDictTypeDetail('script_direction');
expect(lastCall().url).toContain('/dicts/script_direction');
await createDictType({
code: 'delivery_slot',
name: '投放位置',
group: 'platform',
kind: 'list',
industryScoped: false,
description: '',
sortOrder: 10,
});
expect(lastCall().init?.method).toBe('POST');
expect(lastBody()).toMatchObject({ code: 'delivery_slot', industryScoped: false });
await updateDictType('script_direction', { name: '脚本方向2' });
expect(lastCall().init?.method).toBe('PATCH');
expect(lastBody()).toEqual({ name: '脚本方向2' });
await setDictTypeStatus('script_direction', { status: 'disabled' });
expect(lastCall().url).toContain('/dicts/script_direction/status');
expect(lastBody()).toEqual({ status: 'disabled' });
await deleteDictType('script_direction');
expect(lastCall().init?.method).toBe('DELETE');
});
it('条目列表:industry_id 只在传入行业视角时出现', async () => {
vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(jsonResponse(itemList))
.mockResolvedValueOnce(jsonResponse(itemList));
await fetchDictItemList('script_direction', { industry_id: ID });
expect(lastCall().url).toContain('/dicts/script_direction/items?industry_id=');
await fetchDictItemList('script_direction');
expect(lastCall().url).not.toContain('industry_id');
});
it('新建 / 编辑条目:body 用 camelCase,编辑不提交编码与行业', async () => {
vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(jsonResponse(item, 201))
.mockResolvedValueOnce(jsonResponse(item));
await createDictItem('script_direction', {
code: 'oral',
name: '口播',
parentCode: null,
industryId: null,
sortOrder: 10,
status: 'active',
remark: '',
});
expect(lastCall().init?.method).toBe('POST');
expect(lastBody()).toMatchObject({ code: 'oral', parentCode: null, industryId: null });
await updateDictItem('script_direction', ID, {
name: '口播(新)',
sortOrder: 20,
status: 'disabled',
remark: '说明',
});
expect(lastCall().url).toContain(`/dicts/script_direction/items/${ID}`);
expect(lastCall().init?.method).toBe('PATCH');
expect(lastBody()).toEqual({
name: '口播(新)',
sortOrder: 20,
status: 'disabled',
remark: '说明',
});
});
});