119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
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<string, any>, 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<string, any> = 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<any>(),
|
|
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<any, any>({
|
|
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()
|
|
})
|