317 lines
16 KiB
TypeScript
317 lines
16 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { createContext, runInContext } from 'node:vm'
|
|
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { isWorkOrderImage, workOrderAttachmentFilename, workOrderAttachmentName } from '@/utils/workOrderAttachment'
|
|
|
|
function harness() {
|
|
const fetchUserInfo = vi.fn().mockResolvedValue(undefined)
|
|
const act = vi.fn().mockResolvedValue({})
|
|
const detail = vi.fn().mockResolvedValue({ workOrder: { actions: ['reply'], communications: [] } })
|
|
const lifecycle: Record<string, () => unknown> = {}
|
|
const setInterval = vi.fn().mockReturnValue(1)
|
|
const clearInterval = vi.fn()
|
|
const downloadFile = vi.fn()
|
|
const openDocument = vi.fn()
|
|
const fileManager = {
|
|
mkdir: vi.fn((options: { success: () => void }) => options.success()),
|
|
copyFile: vi.fn((options: { srcPath: string, destPath: string, success: () => void }) => options.success()),
|
|
unlink: vi.fn((options: { complete: () => void }) => options.complete()),
|
|
rmdir: vi.fn(),
|
|
}
|
|
const context = createContext({
|
|
setInterval,
|
|
clearInterval,
|
|
exports: {},
|
|
require: (name: string) => {
|
|
if (name === '@dcloudio/uni-app') {
|
|
return {
|
|
onLoad: (callback: () => unknown) => { lifecycle.load = callback },
|
|
onShow: (callback: () => unknown) => { lifecycle.show = callback },
|
|
onHide: (callback: () => unknown) => { lifecycle.hide = callback },
|
|
onUnload: (callback: () => unknown) => { lifecycle.unload = callback },
|
|
}
|
|
}
|
|
if (name === '../api/workOrder')
|
|
return { actOnWorkOrder: act, getWorkOrderDetail: detail }
|
|
if (name === '@/store/user')
|
|
return { useUserStore: () => ({ userInfo: {}, fetchUserInfo }) }
|
|
if (name === '@/utils/workOrderAttachment')
|
|
return { isWorkOrderImage, workOrderAttachmentName, workOrderAttachmentFilename }
|
|
if (name === '@/utils')
|
|
return { getEnvBaseUrl: () => 'https://api.example.com/api/v1' }
|
|
if (name === '@/utils/permission')
|
|
return { hasAnyFeature: (user: { revoked?: boolean }) => !user.revoked }
|
|
return {}
|
|
},
|
|
ref: (value: unknown) => ({ value }),
|
|
computed: (getter: () => unknown) => ({ get value() { return getter() } }),
|
|
nextTick: () => Promise.resolve(),
|
|
definePage: vi.fn(),
|
|
uni: { showToast: vi.fn(), previewImage: vi.fn(), downloadFile, openDocument },
|
|
wx: { env: { USER_DATA_PATH: '/user-data' }, getFileSystemManager: () => fileManager },
|
|
})
|
|
const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8')
|
|
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]?.replace(/\/\/ #ifdef H5[\s\S]*?\/\/ #endif/g, '').replace(/\/\/ #ifndef MP-WEIXIN[\s\S]*?\/\/ #endif/g, '')
|
|
if (!script)
|
|
throw new Error('未找到工单详情脚本')
|
|
runInContext(transpileModule(script, { compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2020 } }).outputText, context)
|
|
runInContext('woNo.value = \'WO-1\'; workOrder.value = { actions: [\'reply\'], communications: [] }', context)
|
|
return { act, detail, fetchUserInfo, lifecycle, setInterval, clearInterval, fileManager, downloadFile, openDocument, evaluate: <Result>(expression: string) => runInContext(expression, context) as Result }
|
|
}
|
|
|
|
describe('工单首次加载状态', () => {
|
|
it('权限和详情请求完成前持续展示骨架屏', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value = undefined')
|
|
expect(page.evaluate('loading.value')).toBe(true)
|
|
let finishUser!: () => void
|
|
let finishDetail!: (value: unknown) => void
|
|
page.fetchUserInfo.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
|
finishUser = resolve
|
|
}))
|
|
page.detail.mockImplementationOnce(() => new Promise((resolve) => {
|
|
finishDetail = resolve
|
|
}))
|
|
const showing = page.lifecycle.show()
|
|
expect(page.evaluate('loading.value')).toBe(true)
|
|
expect(page.detail).not.toHaveBeenCalled()
|
|
finishUser()
|
|
await vi.waitFor(() => expect(page.detail).toHaveBeenCalledOnce())
|
|
expect(page.evaluate('loading.value')).toBe(true)
|
|
finishDetail({ workOrder: { actions: ['reply'], communications: [] } })
|
|
await showing
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
expect(page.evaluate('workOrder.value')).toBeDefined()
|
|
})
|
|
|
|
it('详情请求真正失败后仍可重试', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value = undefined')
|
|
page.detail.mockRejectedValueOnce(new Error('network'))
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
expect(page.evaluate('workOrder.value')).toBeUndefined()
|
|
await page.evaluate<Promise<void>>('loadDetail()')
|
|
expect(page.evaluate('workOrder.value')).toBeDefined()
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
})
|
|
|
|
it('权限不足时结束加载并显示无权限提示', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value = undefined; userStore.userInfo.revoked = true')
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('noPermission.value')).toBe(true)
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
expect(page.detail).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('通知缺少工单编号时结束加载', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value = undefined; woNo.value = ""')
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
expect(page.detail).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('刷新权限时保留已有工单内容', async () => {
|
|
const page = harness()
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('loading.value')).toBe(false)
|
|
expect(page.evaluate('workOrder.value')).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('工单对话回复', () => {
|
|
it('发送后保持窗口打开,清空草稿并允许继续发送', async () => {
|
|
const page = harness()
|
|
page.evaluate('showConversation.value = true; replyContent.value = \'第一条\'')
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
expect(page.evaluate('showConversation.value')).toBe(true)
|
|
expect(page.evaluate('replyContent.value')).toBe('')
|
|
expect(page.evaluate('conversationTarget.value')).toBe('conversation-bottom')
|
|
page.evaluate('replyContent.value = \'第二条\'')
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
expect(page.act).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('发送失败保留文字和附件草稿', async () => {
|
|
const page = harness()
|
|
page.act.mockRejectedValueOnce(new Error('network'))
|
|
page.evaluate('replyContent.value = \'待发送\'; replyAttachments.value = [\'file.pdf\']')
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
expect(page.evaluate('replyContent.value')).toBe('待发送')
|
|
expect(page.evaluate('replyAttachments.value.length')).toBe(1)
|
|
expect(page.evaluate('sending.value')).toBe(false)
|
|
})
|
|
|
|
it('禁止空消息、上传期间发送和无权限回复', async () => {
|
|
const page = harness()
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
page.evaluate('replyContent.value = \'消息\'; uploading.value = true')
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
page.evaluate('uploading.value = false; workOrder.value.actions = []')
|
|
await page.evaluate<Promise<void>>('performAction(\'reply\')')
|
|
expect(page.act).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('工单评价与刷新', () => {
|
|
it('评价使用独立草稿,不把未发送的沟通内容当成评价', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value.actions = ["evaluate"]; replyContent.value = "未发出的沟通"; evaluationContent.value = "服务很好"')
|
|
await page.evaluate<Promise<void>>('performAction("evaluate")')
|
|
expect(page.act).toHaveBeenCalledWith(expect.objectContaining({ action: 'evaluate', content: '服务很好', satisfactionLevel: 5 }))
|
|
expect(page.evaluate('replyContent.value')).toBe('未发出的沟通')
|
|
expect(page.evaluate('evaluationContent.value')).toBe('')
|
|
})
|
|
|
|
it('评价失败保留评价草稿,无效评分不提交', async () => {
|
|
const page = harness()
|
|
page.act.mockRejectedValueOnce(new Error('network'))
|
|
page.evaluate('workOrder.value.actions = ["evaluate"]; evaluationContent.value = "评价草稿"')
|
|
await page.evaluate<Promise<void>>('performAction("evaluate")')
|
|
expect(page.evaluate('evaluationContent.value')).toBe('评价草稿')
|
|
for (const rating of [0, 6, 4.5]) {
|
|
page.evaluate(`workOrder.value.actions = ["evaluate"]; rating.value = ${rating}`)
|
|
await page.evaluate<Promise<void>>('performAction("evaluate")')
|
|
}
|
|
expect(page.act).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('操作成功后等待旧轮询结束,再获取最新详情', async () => {
|
|
const page = harness()
|
|
let finish!: (value: unknown) => void
|
|
page.detail.mockImplementationOnce(() => new Promise((resolve) => {
|
|
finish = resolve
|
|
}))
|
|
page.detail.mockResolvedValueOnce({ workOrder: { actions: ['reply'], currentStatus: 2, communications: [{ id: 2, content: '新回复' }] } })
|
|
const polling = page.evaluate<Promise<void>>('loadDetail(true)')
|
|
page.evaluate('replyContent.value = "新回复"')
|
|
const sending = page.evaluate<Promise<void>>('performAction("reply")')
|
|
await vi.waitFor(() => expect(page.act).toHaveBeenCalledTimes(1))
|
|
expect(page.detail).toHaveBeenCalledTimes(1)
|
|
finish({ workOrder: { actions: ['reply'], currentStatus: 1, communications: [] } })
|
|
await Promise.all([polling, sending])
|
|
expect(page.detail).toHaveBeenCalledTimes(2)
|
|
expect(page.evaluate('workOrder.value.currentStatus')).toBe(2)
|
|
expect(page.evaluate('workOrder.value.communications[0].content')).toBe('新回复')
|
|
})
|
|
})
|
|
|
|
describe('工单附件原名展示', () => {
|
|
it('正文和消息附件都显示名称,不再使用附件序号', () => {
|
|
const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8')
|
|
expect(source.match(/attachmentName\(url\)/g)?.length).toBe(2)
|
|
expect(source).not.toContain('附件 {{ index + 1 }}')
|
|
const page = harness()
|
|
page.evaluate('workOrder.value.attachmentNames = { "random.pdf": "排查说明.pdf" }')
|
|
expect(page.evaluate('attachmentName("random.pdf")')).toBe('排查说明.pdf')
|
|
expect(page.evaluate('attachmentName("old.xlsx")')).toBe('old.xlsx')
|
|
})
|
|
|
|
it('加载历史和轮询刷新后仍保留历史附件原名', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value.communications = [{ id: 20 }]; hasMoreHistory.value = true')
|
|
page.detail.mockResolvedValueOnce({ workOrder: { communications: [{ id: 10 }], attachmentNames: { 'old.pdf': '历史资料.pdf' } } })
|
|
await page.evaluate<Promise<void>>('loadHistory()')
|
|
page.detail.mockResolvedValueOnce({ workOrder: { actions: ['reply'], communications: [{ id: 21 }], attachmentNames: { 'new.xlsx': '新资料.xlsx' } } })
|
|
await page.evaluate<Promise<void>>('loadDetail(true)')
|
|
expect(page.evaluate('attachmentName("old.pdf")')).toBe('历史资料.pdf')
|
|
expect(page.evaluate('attachmentName("new.xlsx")')).toBe('新资料.xlsx')
|
|
expect(page.evaluate('workOrder.value.communications.length')).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('工单问题描述布局', () => {
|
|
it('分开展示文字说明与附件,并在内容为空时显示提示', () => {
|
|
const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8')
|
|
const start = source.indexOf('问题描述')
|
|
const end = source.indexOf('</wd-card>', start)
|
|
const description = source.slice(start, end)
|
|
expect(description).toContain('文字说明')
|
|
expect(description).toContain('相关附件')
|
|
expect(description).toContain('break-words')
|
|
expect(description).not.toContain('break-all')
|
|
expect(description).toContain('暂无问题描述')
|
|
expect(description).toContain('{{ workOrder.attachmentUrls.length }} 个附件')
|
|
})
|
|
})
|
|
|
|
describe('工单附件文件名预览', () => {
|
|
it('微信打开的文件使用工单展示的原始名称,返回后清理本地副本', async () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value.attachmentNames = { "https://files/random": "现场照片.pdf" }')
|
|
page.evaluate('openAttachment("https://files/random")')
|
|
page.evaluate('uni.downloadFile.mock.calls[0][0].success({ statusCode: 200, tempFilePath: "/tmp/random.pdf" })')
|
|
expect(page.fileManager.copyFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
srcPath: '/tmp/random.pdf',
|
|
destPath: expect.stringMatching(/\/现场照片\.pdf$/),
|
|
}))
|
|
expect(page.openDocument).toHaveBeenCalledWith(expect.objectContaining({ filePath: expect.stringMatching(/\/现场照片\.pdf$/) }))
|
|
page.lifecycle.hide()
|
|
await page.lifecycle.show()
|
|
expect(page.fileManager.unlink).toHaveBeenCalledWith(expect.objectContaining({ filePath: expect.stringMatching(/\/现场照片\.pdf$/) }))
|
|
expect(page.fileManager.rmdir).toHaveBeenCalledWith(expect.objectContaining({ recursive: true }))
|
|
})
|
|
})
|
|
|
|
describe('工单照片预览', () => {
|
|
it('正文和沟通附件复用缩略图,原名为照片的随机地址也使用图片预览', () => {
|
|
const page = harness()
|
|
page.evaluate('workOrder.value.attachmentNames = { "/upload/random": "现场照片.JPG" }')
|
|
page.evaluate('openAttachment("/upload/random")')
|
|
expect(page.evaluate('uni.previewImage.mock.calls[0][0]')).toEqual({
|
|
urls: ['https://api.example.com/upload/random'],
|
|
current: 'https://api.example.com/upload/random',
|
|
})
|
|
const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8')
|
|
expect(source.match(/<WorkOrderAttachmentPreview\s/g)).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe('预览照片后保留工单沟通', () => {
|
|
it('预览引起隐藏和重新显示时,保留窗口、草稿及阅读位置并恢复轮询', async () => {
|
|
const page = harness()
|
|
await page.lifecycle.show()
|
|
page.evaluate('showConversation.value = true; replyContent.value = "未发送的内容"; replyAttachments.value = ["photo.jpg"]; conversationTarget.value = "history-message"')
|
|
page.evaluate('openAttachment("photo.jpg")')
|
|
page.lifecycle.hide()
|
|
expect(page.clearInterval).toHaveBeenCalledWith(1)
|
|
expect(page.evaluate('showConversation.value')).toBe(true)
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('showConversation.value')).toBe(true)
|
|
expect(page.evaluate('replyContent.value')).toBe('未发送的内容')
|
|
expect(page.evaluate('replyAttachments.value')).toEqual(['photo.jpg'])
|
|
expect(page.evaluate('conversationTarget.value')).toBe('history-message')
|
|
expect(page.setInterval).toHaveBeenCalledTimes(2)
|
|
page.lifecycle.unload()
|
|
expect(page.evaluate('visible')).toBe(false)
|
|
expect(page.evaluate('refreshTimer')).toBeUndefined()
|
|
})
|
|
|
|
it('从正文预览返回或手动关闭窗口后,不会自动打开沟通', async () => {
|
|
const page = harness()
|
|
page.evaluate('openAttachment("photo.jpg")')
|
|
page.lifecycle.hide()
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('showConversation.value')).toBe(false)
|
|
page.evaluate('showConversation.value = true; showConversation.value = false')
|
|
page.lifecycle.hide()
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('showConversation.value')).toBe(false)
|
|
})
|
|
|
|
it('重新显示时若失去查看权限,关闭沟通窗口并停止轮询', async () => {
|
|
const page = harness()
|
|
await page.lifecycle.show()
|
|
page.evaluate('showConversation.value = true')
|
|
page.lifecycle.hide()
|
|
page.evaluate('userStore.userInfo.revoked = true')
|
|
await page.lifecycle.show()
|
|
expect(page.evaluate('showConversation.value')).toBe(false)
|
|
expect(page.evaluate('noPermission.value')).toBe(true)
|
|
expect(page.evaluate('refreshTimer')).toBeUndefined()
|
|
})
|
|
})
|