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: '说明', }); }); });