147 lines
8.5 KiB
JavaScript
147 lines
8.5 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const test = require('node:test')
|
|
const ts = require('typescript')
|
|
const vue = require('vue')
|
|
const { renderToString } = require('@vue/server-renderer')
|
|
const { parse, compileScript, compileTemplate } = require('@vue/compiler-sfc')
|
|
|
|
const src = path.join(__dirname, '../src')
|
|
function moduleFrom(source, mockRequire = require, browserWindow = {}) {
|
|
const code = ts.transpileModule(source.replaceAll('import.meta.env.DEV', 'false'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
|
}).outputText
|
|
const module = { exports: {} }
|
|
new Function('require', 'module', 'exports', 'window', code)(mockRequire, module, module.exports, browserWindow)
|
|
return module.exports
|
|
}
|
|
const read = (file) => fs.readFileSync(path.join(src, file), 'utf8')
|
|
const types = moduleFrom(read('utils/appointment-type.ts'))
|
|
const { descriptor } = parse(read('components/chat-dialog/index.vue'))
|
|
const script = compileScript(descriptor, { id: 'appointment-call-mode' })
|
|
const template = compileTemplate({ source: descriptor.template.content, filename: 'chat-dialog.vue', id: 'appointment-call-mode', compilerOptions: { bindingMetadata: script.bindings } })
|
|
assert.deepEqual(template.errors, [])
|
|
const render = moduleFrom(template.code).render
|
|
const passthrough = { setup: (_, { slots }) => () => vue.h('div', [slots.default?.(), slots.headerToolbar?.()]) }
|
|
const picker = text => ({ setup: () => () => vue.h('button', text) })
|
|
|
|
function chat(policy = {}) {
|
|
const requests = [], warnings = [], sdkCalls = []
|
|
const guard = moduleFrom(read('utils/appointment-call-guard.ts'))
|
|
const server = Object.fromEntries(['calls', 'call', 'groupCall'].map(name => [name, async params => { sdkCalls.push({ name, params }) }]))
|
|
const api = { getCallSignature: async params => { requests.push(params); return policy } }
|
|
const result = moduleFrom(script.content, name => {
|
|
if (name === 'vue') return { ...vue, onMounted() {}, onUnmounted() {} }
|
|
if (name === '@/utils/appointment-type') return types
|
|
if (name === '@/utils/appointment-call-guard') return guard
|
|
if (name === '@/utils/im-chat-archive-trigger') return moduleFrom(read('utils/im-chat-archive-trigger.ts'))
|
|
if (name === '@/api/tcm') return api
|
|
if (name === '@/utils/feedback') return { default: { msgWarning: text => warnings.push(text) } }
|
|
if (name === '@/stores/modules/user') return { default: () => ({ userInfo: {} }) }
|
|
if (name === '@/utils/call-local-recorder') return { CallLocalRecorder: class {} }
|
|
if (name === '@tencentcloud/chat-uikit-vue3') return {
|
|
useLoginState: () => ({}), useConversationListState: () => ({ activeConversation: vue.ref(null) }),
|
|
Chat: passthrough, UIKitProvider: passthrough, MessageInput: passthrough, MessageList: passthrough,
|
|
EmojiPicker: picker('表情'), ImagePicker: picker('图片'), FilePicker: picker('文件'),
|
|
AudioCallPicker: picker('语音拨打'), VideoCallPicker: picker('视频拨打')
|
|
}
|
|
if (name === '@tencentcloud/call-uikit-vue') return { TUICallKitServer: server, TUICallType: { VIDEO_CALL: 2 }, TUIStore: {}, StoreName: {}, NAME: {} }
|
|
return {}
|
|
}).default.setup({}, { expose() {} })
|
|
result.visible.value = true
|
|
result.isReady.value = true
|
|
result.isCallReady.value = true
|
|
result.patientId.value = 50
|
|
result.diagnosisId.value = 70
|
|
result.appointmentId.value = 90
|
|
result.syncAppointmentCallPolicy(policy)
|
|
guard.registerAppointmentCallGuard(result.prepareAppointmentCall)
|
|
return { state: result, guard, server, requests, warnings, sdkCalls, api }
|
|
}
|
|
const policy = type => ({ appointment_id: 90, appointment_type: type, appointment_type_desc: types.appointmentTypeDescription(type), can_video_call: type === 'video', can_audio_call: type === 'video' || type === 'phone', call_disabled_reason: type === 'text' ? '图文问诊不支持音视频通话' : '当前挂号不支持该通话方式' })
|
|
|
|
test('type labels and video capability preserve legacy video while rejecting text and unknown', () => {
|
|
for (const value of ['text', 'phone', 'unknown']) assert.equal(types.canAppointmentVideoCall(value), false)
|
|
for (const value of ['video', '', ' ', null, undefined]) assert.equal(types.canAppointmentVideoCall(value), true)
|
|
assert.equal(types.appointmentTypeDescription('text'), '图文问诊')
|
|
})
|
|
|
|
test('rendered real chat template keeps text tools while excluding call controls in text mode', async () => {
|
|
async function markup(type) {
|
|
const { state } = chat(policy(type))
|
|
const app = vue.createSSRApp({ render: () => render({}, [], {}, vue.proxyRefs(state)) })
|
|
app.config.warnHandler = () => {}
|
|
for (const name of ['el-tag', 'el-button', 'el-tooltip', 'el-icon', 'el-alert']) app.component(name, passthrough)
|
|
const context = {}
|
|
await renderToString(app, context)
|
|
return context.teleports.body
|
|
}
|
|
const text = await markup('text')
|
|
assert.match(text, /图文问诊/)
|
|
for (const tool of ['表情', '图片', '文件']) assert.match(text, new RegExp(tool))
|
|
assert.doesNotMatch(text, /视频拨打|语音拨打|群视频/)
|
|
const video = await markup('video')
|
|
assert.match(video, /视频问诊/)
|
|
assert.match(video, /视频拨打/)
|
|
assert.match(video, /群视频/)
|
|
})
|
|
|
|
test('text mode hides all live call entries and refuses direct group invocation', async () => {
|
|
const { state, requests, sdkCalls } = chat(policy('text'))
|
|
assert.equal(state.appointmentTypeLabel.value, '图文问诊')
|
|
assert.equal(state.canVideoCall.value, false)
|
|
assert.equal(state.canAudioCall.value, false)
|
|
await state.startGroupVideoCall()
|
|
await assert.rejects(state.prepareAppointmentCall(2), /图文问诊/)
|
|
assert.deepEqual(requests, [])
|
|
assert.deepEqual(sdkCalls, [])
|
|
assert.match(descriptor.template.content, /<VideoCallPicker v-if="isCallReady && canVideoCall"/)
|
|
assert.match(descriptor.template.content, /<AudioCallPicker v-if="isCallReady && canAudioCall"/)
|
|
})
|
|
|
|
test('all SDK outgoing paths stop before touching SDK when current appointment is text', async () => {
|
|
const { state, server, sdkCalls } = chat(policy('text'))
|
|
state.installTUICallKitRoomHooks()
|
|
for (const method of ['call', 'calls', 'groupCall']) await assert.rejects(server[method]({ type: 2 }), /图文问诊/)
|
|
assert.deepEqual(sdkCalls, [])
|
|
})
|
|
|
|
test('video preflight uses actual appointment and prevents a server-side change to text', async () => {
|
|
const { state, requests, api } = chat(policy('video'))
|
|
assert.equal(state.canVideoCall.value, true)
|
|
await state.prepareAppointmentCall(2)
|
|
assert.deepEqual(requests, [{ patient_id: 50, diagnosis_id: 70, appointment_id: 90 }])
|
|
api.getCallSignature = async () => policy('text')
|
|
await assert.rejects(state.prepareAppointmentCall(2), /图文问诊/)
|
|
assert.equal(state.canVideoCall.value, false)
|
|
assert.equal(state.appointmentTypeLabel.value, '图文问诊')
|
|
})
|
|
|
|
test('historical phone is audio-only; missing server confirmation never enables video', async () => {
|
|
const { state } = chat(policy('phone'))
|
|
assert.equal(state.canAudioCall.value, true)
|
|
assert.equal(state.canVideoCall.value, false)
|
|
await state.prepareAppointmentCall(1)
|
|
await assert.rejects(state.prepareAppointmentCall(2))
|
|
state.syncAppointmentCallPolicy({ appointment_type: 'video' })
|
|
assert.equal(state.canVideoCall.value, false)
|
|
state.syncAppointmentCallPolicy({ appointment_type: null, appointment_type_desc: '未挂号', can_video_call: false, can_audio_call: false })
|
|
assert.equal(state.appointmentTypeLabel.value, '未挂号')
|
|
assert.equal(state.canVideoCall.value, false)
|
|
})
|
|
|
|
test('singleton call guard switches to current chat and cancels a pending old context', async () => {
|
|
const guard = moduleFrom(read('utils/appointment-call-guard.ts'))
|
|
let resolveOld
|
|
const releaseOld = guard.registerAppointmentCallGuard(() => new Promise(resolve => { resolveOld = resolve }))
|
|
const pending = guard.checkAppointmentOutgoingCall({ type: 2 })
|
|
const releaseNew = guard.registerAppointmentCallGuard(async () => { throw new Error('图文问诊') })
|
|
releaseOld()
|
|
await assert.rejects(guard.checkAppointmentOutgoingCall({ type: 2 }), /图文问诊/)
|
|
resolveOld()
|
|
await assert.rejects(pending, /问诊已切换/)
|
|
releaseNew()
|
|
await assert.rejects(guard.checkAppointmentOutgoingCall({ type: 2 }), /先打开/)
|
|
})
|