fix(followup-audio): keep entry visible during capability failures
This commit is contained in:
@@ -268,7 +268,7 @@ test('authenticated audio object URLs revoked on hide and stale blobs never crea
|
||||
|
||||
test('editor capability gate, dirty guard and successful apply hook wire diagnosis plus daily refresh', () => {
|
||||
const source = fs.readFileSync(path.join(componentDir, '../edit.vue'), 'utf8')
|
||||
assert.match(source, /v-if="followupCapabilities\?\.enabled && formData.id"/)
|
||||
assert.match(source, /<el-tab-pane v-if="formData.id" label="回访录音"/)
|
||||
assert.match(source, /:before-leave="beforeTabLeave"/)
|
||||
assert.match(source, /next === 'followupAudio' && basicDirty.value/)
|
||||
assert.match(source, /await loadDiagnosisDetailIntoForm\(diagnosisId, true\)/)
|
||||
@@ -1410,7 +1410,7 @@ test('actual patient editor outside preview allowlist still opens and edits its
|
||||
await editor.handleSubmit()
|
||||
assert.equal(edits.length, 1, 'ordinary diagnosis mock API remains usable; no real patient write')
|
||||
assert.equal(edits[0].remark, '合成普通表单编辑')
|
||||
assert.match(descriptor.template.content, /v-if="followupCapabilities\?\.enabled && formData.id"/)
|
||||
assert.match(descriptor.template.content, /<el-tab-pane v-if="formData.id" label="回访录音"/)
|
||||
} finally { scope.stop() }
|
||||
})
|
||||
|
||||
@@ -1848,3 +1848,163 @@ test('autosave denial for remotely deleted task clears local playback without an
|
||||
await settle();await f.panel.openTask(1);await f.panel.loadAudio();f.panel.reviewItems.value[0].values.fasting_blood_sugar=6.9;await f.panel.saveDraft(true);await settle()
|
||||
assert.equal(f.panel.selectedId.value,0);assert.equal(f.panel.audioUrl.value,'');assert.match(f.panel.errorMessage.value,/录音已删除/);f.dispose()
|
||||
})
|
||||
|
||||
// Execute the real editor script and its actual tab template, with deferred network I/O.
|
||||
function setupAudioEntry() {
|
||||
const requests = [], warnings = [], hooks = {}, exposed = {}
|
||||
const filename = path.join(componentDir, '../edit.vue')
|
||||
const descriptor = parse(fs.readFileSync(filename, 'utf8'), { filename }).descriptor
|
||||
const script = compileScript(descriptor, { id: 'entry-runtime' })
|
||||
const panelComponent = { name: 'FollowupAudioPanel' }
|
||||
const module = execute(script.content, name => {
|
||||
if (name === 'vue') return { ...vue, onBeforeUnmount: callback => { hooks.unmount = callback } }
|
||||
if (name === '@/api/tcm') return { tcmDiagnosisDetail: async ({ id }) => ({ id: String(id), patient_id: String(id + 100), phone: '', id_card: '', remark: '原始备注' }) }
|
||||
if (name === '@/api/app') return { getDictData: async () => ({}) }
|
||||
if (name === '@/api/followupAudio') return { followupAudioCapabilities: (id, signal) => { const pending = deferred(); requests.push({ id, signal, ...pending }); return pending.promise } }
|
||||
if (name === '@/utils/feedback') return { default: { msgWarning: message => warnings.push(message) } }
|
||||
if (name === '@/utils/perm') return { hasPermission: () => true }
|
||||
if (name === '@/stores/modules/user') return { default: () => ({ userInfo: { id: 1 } }) }
|
||||
if (name === '@/stores/modules/app') return { default: () => ({ getImageUrl: value => value }) }
|
||||
if (name === 'element-plus') return { ElMessage: { warning() {} } }
|
||||
if (name === '@element-plus/icons-vue' || name === '@/api/patient') return {}
|
||||
if (name.endsWith('FollowupAudioPanel.vue')) return { default: panelComponent }
|
||||
if (name.endsWith('.vue')) return { default: {} }
|
||||
throw new Error(`Unexpected entry import: ${name}`)
|
||||
})
|
||||
const tab = descriptor.template.content.match(/<el-tab-pane[^>]+name="followupAudio"[\s\S]*?<\/el-tab-pane>/)[0]
|
||||
const compiled = compileTemplate({ source: tab, filename, id: 'entry-tab', compilerOptions: { bindingMetadata: script.bindings } })
|
||||
assert.deepEqual(compiled.errors, [])
|
||||
const render = execute(compiled.code, () => ({ ...vue, resolveComponent: name => name })).render
|
||||
const scope = vue.effectScope()
|
||||
const editor = scope.run(() => module.default.setup({}, { expose: value => Object.assign(exposed, value), emit() {} }))
|
||||
const nodes = () => {
|
||||
const collected = []
|
||||
const visit = node => {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (Array.isArray(node)) { node.forEach(visit); return }
|
||||
collected.push(node)
|
||||
visit(node.children)
|
||||
if (node.children?.default) visit(node.children.default())
|
||||
}
|
||||
visit(render({}, [], {}, vue.proxyRefs(editor)))
|
||||
return collected
|
||||
}
|
||||
return { editor, exposed, requests, warnings, nodes, hasTab: () => nodes().some(node => node.type === 'el-tab-pane'), hasPanel: () => nodes().some(node => node.type === panelComponent), dispose: () => { hooks.unmount(); scope.stop() } }
|
||||
}
|
||||
|
||||
test('saved diagnosis keeps audio entry during pending/error; manual retry recovers without losing dirty input', async () => {
|
||||
const f = setupAudioEntry()
|
||||
try {
|
||||
await f.editor.open('edit', 10)
|
||||
assert.equal(f.hasTab(), true)
|
||||
assert.equal(f.hasPanel(), false)
|
||||
assert.equal(f.editor.followupCapabilitiesLoading.value, true)
|
||||
assert.ok(f.nodes().some(node => node.props?.title === '正在检查回访录音状态…'))
|
||||
f.requests[0].reject(new Error('network offline')); await settle()
|
||||
assert.equal(f.hasTab(), true)
|
||||
assert.equal(f.hasPanel(), false)
|
||||
assert.equal(f.editor.followupCapabilitiesLoading.value, false)
|
||||
assert.match(f.editor.followupCapabilitiesError.value, /加载失败/)
|
||||
assert.equal(f.requests.length, 1, 'no automatic retry')
|
||||
f.editor.formData.value.remark = '保留未保存输入'
|
||||
assert.equal(await f.editor.beforeTabLeave('followupAudio', 'basic'), false)
|
||||
const retryButton = f.nodes().find(node => node.type === 'el-button')
|
||||
const retry = retryButton.props.onClick()
|
||||
const duplicate = f.editor.loadFollowupCapabilities()
|
||||
assert.equal(f.requests.length, 2, 'rapid retries share the in-flight context')
|
||||
assert.equal(f.editor.followupCapabilitiesError.value, '')
|
||||
assert.equal(f.hasPanel(), false)
|
||||
f.requests[1].resolve(caps()); await retry; await duplicate
|
||||
assert.equal(f.hasTab(), true)
|
||||
assert.equal(f.hasPanel(), true)
|
||||
assert.equal(f.editor.formData.value.remark, '保留未保存输入')
|
||||
assert.equal(f.editor.basicDirty.value, true)
|
||||
assert.equal(f.editor.canApplyFollowup(), false)
|
||||
assert.equal(await f.editor.beforeTabLeave('followupAudio', 'basic'), false)
|
||||
} finally { f.dispose() }
|
||||
})
|
||||
|
||||
test('disabled/scope-denied and permission-denied capability responses preserve entry without a panel', async () => {
|
||||
for (const response of [{ ...caps(), enabled: false }, { ...caps(), enabled: false, test_scope_allowed: false }, new Error('403 permission denied'), null]) {
|
||||
const f = setupAudioEntry()
|
||||
try {
|
||||
await f.editor.open('edit', 10)
|
||||
if (response instanceof Error) f.requests[0].reject(response)
|
||||
else f.requests[0].resolve(response)
|
||||
await settle()
|
||||
assert.equal(f.hasTab(), true)
|
||||
assert.equal(f.hasPanel(), false)
|
||||
assert.equal(f.editor.followupCapabilitiesLoading.value, false)
|
||||
const title = f.nodes().find(node => node.type === 'el-alert').props.title
|
||||
assert.match(title, response instanceof Error || response === null ? /加载失败/ : /未启用|未开放/)
|
||||
assert.equal(f.requests.length, 1)
|
||||
} finally { f.dispose() }
|
||||
}
|
||||
})
|
||||
|
||||
test('old capability success/error/finally cannot affect a new patient or same-ID reopened context', async () => {
|
||||
for (const oldFails of [false, true]) for (const id of [10, 20]) {
|
||||
const f = setupAudioEntry()
|
||||
try {
|
||||
await f.editor.open('edit', 10)
|
||||
const old = f.requests[0]
|
||||
const opening = f.editor.open('edit', id)
|
||||
assert.equal(old.signal.aborted, true, 'open resets before awaiting dictionaries')
|
||||
assert.equal(f.editor.followupCapabilities.value, null)
|
||||
await opening
|
||||
assert.equal(f.requests.length, 2)
|
||||
if (oldFails) old.reject(new Error('late old error'))
|
||||
else old.resolve(caps())
|
||||
await settle()
|
||||
assert.equal(f.editor.followupCapabilities.value, null)
|
||||
assert.equal(f.editor.followupCapabilitiesError.value, '')
|
||||
assert.equal(f.editor.followupCapabilitiesLoading.value, true, 'old finally cannot end new loading')
|
||||
assert.equal(f.hasPanel(), false)
|
||||
f.requests[1].resolve({ ...caps(), enabled: false }); await settle()
|
||||
assert.equal(f.editor.followupCapabilitiesDiagnosisId.value, id)
|
||||
assert.equal(f.editor.followupCapabilities.value.enabled, false)
|
||||
assert.equal(f.hasPanel(), false)
|
||||
} finally { f.dispose() }
|
||||
}
|
||||
})
|
||||
|
||||
test('successful capabilities are immediately invalidated on patient switch; late retry completion stays isolated', async () => {
|
||||
const f = setupAudioEntry()
|
||||
try {
|
||||
await f.editor.open('edit', 10); f.requests[0].resolve(caps()); await settle()
|
||||
assert.equal(f.hasPanel(), true)
|
||||
f.editor.formData.value.id = '20'
|
||||
assert.equal(f.hasPanel(), false)
|
||||
assert.equal(f.editor.followupCapabilities.value, null)
|
||||
f.requests[1].reject(new Error('second patient error')); await settle()
|
||||
const retry = f.editor.loadFollowupCapabilities()
|
||||
await f.editor.openViewOnly(30)
|
||||
assert.equal(f.requests[2].signal.aborted, true)
|
||||
f.requests[3].resolve(caps()); await settle()
|
||||
f.requests[2].reject(new Error('old retry error')); await retry
|
||||
assert.equal(f.hasPanel(), true)
|
||||
assert.equal(f.editor.followupCapabilitiesDiagnosisId.value, 30)
|
||||
assert.equal(f.editor.followupCapabilitiesError.value, '')
|
||||
assert.equal(f.editor.viewOnly.value, true)
|
||||
} finally { f.dispose() }
|
||||
})
|
||||
|
||||
test('close and unmount abort pending capabilities and prevent any late state resurrection', async () => {
|
||||
for (const unmount of [false, true]) for (const fails of [false, true]) {
|
||||
const f = setupAudioEntry()
|
||||
try {
|
||||
await f.editor.open('edit', 10)
|
||||
if (unmount) f.dispose()
|
||||
else await f.editor.handleClose()
|
||||
assert.equal(f.requests[0].signal.aborted, true)
|
||||
if (fails) f.requests[0].reject(new Error('late close failure'))
|
||||
else f.requests[0].resolve(caps())
|
||||
await settle()
|
||||
assert.equal(f.editor.followupCapabilities.value, null)
|
||||
assert.equal(f.editor.followupCapabilitiesLoading.value, false)
|
||||
assert.equal(f.editor.followupCapabilitiesError.value, '')
|
||||
assert.equal(f.hasPanel(), false)
|
||||
if (!unmount) assert.equal(f.hasTab(), false)
|
||||
} finally { f.dispose() }
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user