import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('element-plus', () => { const messageBox: any = { confirm: vi.fn(() => { const returned: any = { catch: vi.fn() } returned.then = (handler: Function) => { handler() return returned } return returned }) } return { ElMessageBox: messageBox, ElTag: { name: 'ElTag' } } }) vi.mock('../../../script/serverApi', () => ({ serverApi: vi.fn() })) vi.mock('../../../script/requestPayload', () => ({ requestPayload: vi.fn((params, sort) => ({ params, sort, wrapped: true })) })) vi.mock('../../../script/storage', () => ({ __getItem: vi.fn((name: string) => { if (name === 'serveConfig') { return { token: 'authKey' } } if (name === 'x-auth-token') { return 'token-value' } return undefined }) })) vi.mock('../../../api/index', () => ({ default: { queryApproveConfig: 'queryApproveConfig', queryCrossTenantModuleConfig: 'queryCrossTenantModuleConfig' } })) import { ElMessageBox, ElTag } from 'element-plus' import approvalApi from '../../../api/index' import { requestPayload } from '../../../script/requestPayload' import { serverApi } from '../../../script/serverApi' import { buildURL, checkFiledExistInArray, confirm, createTag, deepAssign, deepCopy, deleteObject, downloadBlobFile, duplicateRemoval, filterButtonHandler, formatterCurrentTime, getArrDifDifferentValue, getFieldValue, getNameByCode, getTreeObjById, globalFormValue, isEmptyStr, isPromise, isValidKey, paramsInterfaceValidKey, someButtonHandler } from '../../../script/util' import { getModelApproval, getCrossTenantModuleConfig } from '../../../script/businessUtil' describe('util helpers', () => { const originalInfo = (window as any).info const originalLoadButtons = (window as any).loadButtons const originalRoute = (window as any).route const originalT = (window as any).$t const originalPathname = window.location.pathname beforeEach(() => { window.sessionStorage.clear() window.localStorage.clear() ;(window as any).info = { secDir: '' } ;(window as any).loadButtons = undefined ;(window as any).route = undefined ;(window as any).$t = vi.fn((text: string) => text) window.history.replaceState({}, '', '/page/list') }) afterEach(() => { window.sessionStorage.clear() window.localStorage.clear() vi.clearAllMocks() vi.useRealTimers() if (originalInfo === undefined) { delete (window as any).info } else { ;(window as any).info = originalInfo } if (originalLoadButtons === undefined) { delete (window as any).loadButtons } else { ;(window as any).loadButtons = originalLoadButtons } if (originalRoute === undefined) { delete (window as any).route } else { ;(window as any).route = originalRoute } if (originalT === undefined) { delete (window as any).$t } else { ;(window as any).$t = originalT } window.history.replaceState({}, '', originalPathname) }) it('isValidKey 和 paramsInterfaceValidKey 能按键复制对象属性', () => { const params = { name: 'tom', age: 18 } const target: any = { role: 'admin' } expect(isValidKey('name', params)).toBe(true) expect(isValidKey('missing', params)).toBe(false) paramsInterfaceValidKey(target, params) expect(target).toEqual({ role: 'admin', name: 'tom', age: 18 }) }) it('globalFormValue 支持不同层级的 get/set,并在层级过深时抛错', () => { const data: any = { name: 'tom', user: { profile: { score: 95, detail: { city: 'shanghai' } } } } const rootField = globalFormValue(data, 'name', 0) expect(rootField._get()).toBe('tom') rootField._set('jack') expect(data.name).toBe('jack') const splitField = globalFormValue(data, 'user.profile.score', 1) expect(splitField._get()).toBeUndefined() splitField._set(100) expect(data.user['profile.score']).toBe(100) const deepField = globalFormValue(data, 'user.profile.detail.city') expect(deepField._get()).toBe('shanghai') deepField._set('beijing') expect(data.user.profile.detail.city).toBe('beijing') const tooDeep = globalFormValue({ a: { b: { c: { d: { e: 1 } } } } } as any, 'a.b.c.d.e') expect(() => tooDeep._set(2)).toThrow(/层级太深/) }) it('isPromise 能识别 Promise 风格对象', () => { expect(isPromise(Promise.resolve())).toBe(true) expect(isPromise({ then: () => {}, catch: () => {} } as any)).toBe(true) expect(isPromise({ then: () => {} } as any)).toBe(false) expect(isPromise(null as any)).toBeNull() }) it('getTreeObjById 支持递归查找和自定义 children 键', () => { const tree = [ { id: '1', children: [ { id: '1-1', children: [] } ] } ] const customTree = [ { id: 'root', nodes: [ { id: 'leaf' } ] } ] expect(getTreeObjById(tree as any, '1-1')).toEqual({ id: '1-1', children: [] }) expect(getTreeObjById(customTree as any, 'leaf', 'nodes')).toEqual({ id: 'leaf' }) expect(getTreeObjById(tree as any, '404')).toBeUndefined() }) it('confirm 会调用 ElMessageBox.confirm', async () => { const callback = vi.fn() const confirmMock = vi.mocked((ElMessageBox as any).confirm) confirmMock.mockResolvedValueOnce(undefined as any) confirm('deleteTips', callback) await new Promise(resolve => setTimeout(resolve, 0)) expect(confirmMock).toHaveBeenCalledWith('deleteTips', 'confirmTips') expect((window as any).$t).toHaveBeenNthCalledWith(1, 'deleteTips') expect((window as any).$t).toHaveBeenNthCalledWith(2, 'confirmTips') expect(callback).toHaveBeenCalled() }) it('createTag 会透传 ElTag 及默认参数', () => { const h = vi.fn().mockReturnValue('rendered-tag') const result = createTag(h, { type: 'success', text: '已启用', isBorder: '1', color: '#67c23a' }) expect(result).toBe('rendered-tag') expect(h).toHaveBeenCalledWith( ElTag, { type: 'success', hit: false, size: 'medium', effect: 'light', color: '#67c23a', style: { borderWidth: '1px' } }, '已启用' ) }) it('checkFiledExistInArray 能返回匹配项,并在参数非法时抛错', () => { const list = [ { name: 'zhangSan', age: 20 }, { name: 'liSi', age: 21 } ] expect(checkFiledExistInArray('liSi', list, 'name')).toEqual({ name: 'liSi', age: 21 }) expect(() => checkFiledExistInArray(undefined as any, list, 'name')).toThrow(/undefined/) expect(() => checkFiledExistInArray('tom', undefined as any, 'name')).toThrow(/数组/) expect(() => checkFiledExistInArray('tom', [{ age: 18 }], 'name')).toThrow(/不存在.*name/) }) it('buildURL 会拼接 baseURL、token 和额外查询参数,并跳过空值', () => { window.sessionStorage.setItem('serveConfig', JSON.stringify({ baseURL: '/gateway' })) expect(buildURL('/user/list', { keyword: 'tom', empty: '', page: 1 })).toBe( '/gateway/user/list?authKey=token-value&keyword=tom&page=1' ) expect(buildURL('/user/list?status=1', { keyword: 'tom' })).toBe( '/gateway/user/list?status=1&authKey=token-value&keyword=tom' ) expect(buildURL('')).toBe('') }) it('deepAssign 会递归合并对象、替换数组、删除多余字段并补充缺失字段', () => { const target: any = { base: { name: 'old', extra: 'remove' }, list: [1, 2], keep: 'value', removeMe: true } const source: any = { base: { name: 'new' }, list: [3, 4], keep: 'next', addMe: 100 } const result = deepAssign(target, source) expect(result).toBe(target) expect(result).toEqual({ base: { name: 'new' }, list: [3, 4], keep: 'next', addMe: 100 }) }) it('filterButtonHandler 和 someButtonHandler 会按路由权限过滤按钮', () => { ;(window as any).info = { secDir: '/tenant' } ;(window as any).loadButtons = { '/page/list': ['edit', 'delete'], '/fallback/list': ['export'] } ;(window as any).route = { path: '/fallback/list/:id', params: { id: '123' } } window.history.replaceState({}, '', '/tenant/page/list') expect(filterButtonHandler()).toBe(true) expect(filterButtonHandler('edit')).toBe(true) expect(filterButtonHandler('add')).toBe(false) window.history.replaceState({}, '', '/tenant/fallback/list/123') expect(filterButtonHandler('export')).toBe(true) expect(someButtonHandler([{ code: 'export' }, { code: 'hidden' }, {}])).toBe(3) delete (window as any).loadButtons expect(someButtonHandler([{ code: 'a' }, { code: 'b' }])).toBe(2) }) it('deepCopy、getFieldValue、deleteObject 和 isEmptyStr 保持当前实现行为', () => { const source = { user: { profile: { name: 'tom' } } } const copied = deepCopy(source) copied.user.profile.name = 'jack' expect(source.user.profile.name).toBe('tom') expect(getFieldValue({ user: { name: 'lucy' } } as any, 'user.name')).toBe('lucy') const list = [ { code: 'A' }, { code: 'B' }, { code: 'A' } ] deleteObject('A', list as any, 'code') expect(list).toEqual([{ code: 'B' }]) expect(() => deleteObject('A', [{ id: 1 }] as any, 'code')).toThrow(/不存在/) expect(isEmptyStr('')).toBe(true) expect(isEmptyStr(null)).toBe(true) expect(isEmptyStr(undefined)).toBe(true) expect(isEmptyStr(0)).toBe(false) }) it('duplicateRemoval、formatterCurrentTime、getArrDifDifferentValue 和 getNameByCode 返回预期结果', () => { const list = [ { id: 1, label: 'A' }, { id: 1, label: 'A-2' }, { id: 2, label: 'B' } ] expect(duplicateRemoval(list as any, 'id')).toEqual([{ id: 2, label: 'B' }]) expect(list).toEqual([{ id: 2, label: 'B' }]) const date = new Date(2024, 0, 2, 3, 4, 5) expect(formatterCurrentTime(date, 'yyyy-MM-DD')).toBe('2024-01-02') expect(formatterCurrentTime(date, 'yyyy-MM-DD-hh')).toBe('2024-01-02-03') expect(formatterCurrentTime(date, 'yyyy-MM-DD-hh-mm-ss')).toBe('2024-01-02-03-04-05') expect(formatterCurrentTime(date)).toBe('2024-01-02') expect( getArrDifDifferentValue( [{ id: 1, name: 'A' }, { id: 2, name: 'B' }], [{ id: 2, name: 'B2' }, { id: 3, name: 'C' }], 'id' ) ).toEqual([{ id: 1, name: 'A' }, { id: 3, name: 'C' }]) expect( getArrDifDifferentValue( [{ id: 1, name: 'A' }, { id: 2, name: 'B' }], [{ id: 2, name: 'B2' }, { id: 3, name: 'C' }], 'id', 'intersection' ) ).toEqual([{ id: 2, name: 'B' }]) expect(getNameByCode([{ value: 1, label: '启用' }], 1)).toBe('启用') expect(getNameByCode([{ code: 'x', name: '名称' }], 'x', 'code', 'name')).toBe('名称') expect(getNameByCode([], 'x')).toBe('') }) it('getModelApproval 和 getCrossTenantModuleConfig 会使用 requestPayload 与 serverApi', () => { const approveCallback = vi.fn() const crossTenantCallback = vi.fn() const requestPayloadMock = vi.mocked(requestPayload) const serverApiMock = vi.mocked(serverApi) getModelApproval('user', approveCallback) getCrossTenantModuleConfig('dept', crossTenantCallback) expect(requestPayloadMock).toHaveBeenNthCalledWith(1, { code: 'user' }, { module: 'DESC' }) expect(requestPayloadMock).toHaveBeenNthCalledWith(2, { code: 'dept' }, { module: 'DESC' }) expect(serverApiMock).toHaveBeenNthCalledWith(1, { params: { params: { code: 'user' }, sort: { module: 'DESC' }, wrapped: true }, interface: approvalApi.queryApproveConfig, success: expect.any(Function) }) expect(serverApiMock).toHaveBeenNthCalledWith(2, { params: { params: { code: 'dept' }, sort: { module: 'DESC' }, wrapped: true }, interface: approvalApi.queryCrossTenantModuleConfig, success: expect.any(Function) }) const firstSuccess = serverApiMock.mock.calls[0][0].success const secondSuccess = serverApiMock.mock.calls[1][0].success firstSuccess({ total: 1 }) secondSuccess({ enabled: true }) expect(approveCallback).toHaveBeenCalledWith({ total: 1 }) expect(crossTenantCallback).toHaveBeenCalledWith({ enabled: true }) }) it('downloadBlobFile 会创建下载链接并在异步阶段清理资源', () => { vi.useFakeTimers() const createObjectURL = vi.fn().mockReturnValue('blob:demo') const revokeObjectURL = vi.fn() const appendChild = vi.spyOn(document.body, 'appendChild') const removeChild = vi.spyOn(document.body, 'removeChild') const click = vi.fn() const link = document.createElement('a') vi.spyOn(link, 'click').mockImplementation(click) const createElement = vi.spyOn(document, 'createElement').mockReturnValue(link) Object.defineProperty(window.URL, 'createObjectURL', { value: createObjectURL, configurable: true }) Object.defineProperty(window.URL, 'revokeObjectURL', { value: revokeObjectURL, configurable: true }) downloadBlobFile('content', 'demo.txt') expect(createElement).toHaveBeenCalledWith('a') expect(link.href).toBe('blob:demo') expect(link.download).toBe('demo.txt') expect(appendChild).toHaveBeenCalledWith(link) expect(click).toHaveBeenCalled() vi.runAllTimers() expect(removeChild).toHaveBeenCalledWith(link) expect(revokeObjectURL).toHaveBeenCalledWith('blob:demo') }) })