import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { parse, compileScript } from '@vue/compiler-sfc' import ts from 'typescript' import * as vue from 'vue' function compiledSfc(path: string, dependencies: Record, inlineTemplate = true) { const descriptor = parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor const compiled = compileScript(descriptor, { id: path, inlineTemplate }) const source = ts.createSourceFile('compiled.ts', compiled.content, ts.ScriptTarget.Latest, true) const env: Record = Object.fromEntries( Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default') ) Object.assign(env, dependencies) for (const statement of source.statements) { if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly) continue const clause = statement.importClause if (clause?.name) env[clause.name.text] = dependencies[clause.name.text] || {} if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { for (const item of clause.namedBindings.elements) { if (item.isTypeOnly) continue const name = (item.propertyName || item.name).text env[item.name.text] = statement.moduleSpecifier.getText(source) === '"vue"' || statement.moduleSpecifier.getText(source) === "'vue'" ? (vue as any)[name] : dependencies[name] } } } const code = ts.transpile( source.statements .filter((s) => !ts.isImportDeclaration(s)) .map((s) => s.getText(source)) .join('\n') .replace('export default', 'const component ='), { target: ts.ScriptTarget.ESNext } ) return new Function(...Object.keys(env), `${code}; return component`)(...Object.values(env)) } for (const outcome of ['success', 'failure'] as const) test(`real attachment ${outcome} releases reactive external save guard`, async () => { let resolve!: (value: any) => void, reject!: (reason: any) => void const pending = new Promise((a, b) => { resolve = a reject = b }) const events: string[] = [] const browser = { addEventListener() {}, removeEventListener() {} } const Attachment = compiledSfc( '../src/components/business/attachment-upload-field/index.vue', { useRoute: () => ({ path: '/pl/legal-use-document' }), isMpWebviewPath: () => false, fetchUploadAttachment: () => pending, ElMessage: { warning: (v: string) => events.push(v), error: (v: string) => events.push(v) }, document: browser, window: browser }, false ) Attachment.render = () => null const upload = vue.ref(), model = vue.ref('') // An external footer/computed consumes the exposed guard independently of v-model. const savingDisabled = vue.computed(() => !!upload.value?.hasUploading()) const buttons: any[] = [] const renderer = vue.createRenderer({ createElement: (tag: string) => { const el: any = { tag } if (tag === 'button') buttons.push(el) return el }, createText: () => ({}), createComment: () => ({}), setText() {}, setElementText() {}, parentNode: () => null, nextSibling: () => null, insert() {}, remove() {}, patchProp: (el: any, key: string, _prev: any, value: any) => { el[key] = value } }) const app = renderer.createApp({ render: () => vue.h('section', [ vue.h(Attachment, { ref: upload, modelValue: model.value, 'onUpdate:modelValue': (value: string) => { model.value = value } }), vue.h('button', { disabled: savingDisabled.value }, '保存本段资料') ]) }) app.mount({}) await vue.nextTick() assert.equal(buttons[0].disabled, false) const operation = upload.value.$.setupState.handleAttachmentChange({ raw: new File(['synthetic'], 'proof.pdf', { type: 'application/pdf' }) }) await vue.nextTick() assert.equal(buttons[0].disabled, true) assert.equal(upload.value.hasUploading(), true) if (outcome === 'success') resolve({ fileUrl: 'legal/proof.pdf', fileName: 'proof.pdf', fileType: 'application/pdf' }) else reject(Error('上传失败')) await operation await vue.nextTick() assert.equal(upload.value.hasUploading(), false) assert.equal(buttons[0].disabled, false, '上传结束必须通知外部保存按钮更新') assert.equal(model.value, outcome === 'success' ? 'legal/proof.pdf' : '') app.unmount() })