|
|
|
@@ -0,0 +1,110 @@
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import vm from 'node:vm'
|
|
|
|
|
import * as Vue from 'vue'
|
|
|
|
|
import { compile } from '@vue/compiler-dom'
|
|
|
|
|
import { projectValidation } from './project-assignment.js'
|
|
|
|
|
|
|
|
|
|
const source = readFileSync(new URL('../views/market/MarketModuleForm.vue', import.meta.url), 'utf8')
|
|
|
|
|
// Render the real loop/slot/ref structure; native host nodes need no browser or service.
|
|
|
|
|
const template = source.slice(source.indexOf('<template v-for="section in leadingSections"'), source.indexOf('\n\n <section'))
|
|
|
|
|
const render = new Function('Vue', compile(template, { prefixIdentifiers: true }).code)(Vue)
|
|
|
|
|
const submitSource = source.slice(source.indexOf('async function submit()'), source.indexOf('\nasync function confirmLeave()'))
|
|
|
|
|
const renderer = Vue.createRenderer({
|
|
|
|
|
createElement: () => ({}), createText: () => ({}), createComment: () => ({}),
|
|
|
|
|
insert(node, parent, anchor = null) {
|
|
|
|
|
parent.children ||= []
|
|
|
|
|
node.parent = parent
|
|
|
|
|
const index = parent.children.indexOf(anchor)
|
|
|
|
|
parent.children.splice(index < 0 ? parent.children.length : index, 0, node)
|
|
|
|
|
},
|
|
|
|
|
remove(node) {
|
|
|
|
|
const siblings = node.parent?.children || []
|
|
|
|
|
const index = siblings.indexOf(node)
|
|
|
|
|
if (index >= 0) siblings.splice(index, 1)
|
|
|
|
|
},
|
|
|
|
|
setText() {}, setElementText() {}, patchProp() {},
|
|
|
|
|
parentNode: (node) => node.parent,
|
|
|
|
|
nextSibling: (node) => {
|
|
|
|
|
const siblings = node.parent?.children || []
|
|
|
|
|
return siblings[siblings.indexOf(node) + 1] || null
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function mountForm({ role, mode = 'create', projectId = 'project-1', loading = false, error = '' }) {
|
|
|
|
|
const projectAssignment = Vue.ref(null)
|
|
|
|
|
const formData = Vue.ref({ source: 'research_donation', projectId })
|
|
|
|
|
const messages = [], requests = []
|
|
|
|
|
let validations = 0
|
|
|
|
|
const ProjectAssignmentField = {
|
|
|
|
|
setup(_, { expose }) {
|
|
|
|
|
expose({ validate() {
|
|
|
|
|
validations++
|
|
|
|
|
return projectValidation({ editing: mode === 'edit', required: true, projectId, loading, error })
|
|
|
|
|
} })
|
|
|
|
|
return () => Vue.h('div')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const Section = { setup: (_, { slots }) => () => slots.insert?.() }
|
|
|
|
|
const context = Vue.ref({ module: 'protocol', marketRole: role })
|
|
|
|
|
const app = renderer.createApp({
|
|
|
|
|
components: { MarketFormSection: Section, ProjectAssignmentField },
|
|
|
|
|
setup: () => ({
|
|
|
|
|
projectAssignment, context, formData, mode,
|
|
|
|
|
leadingSections: [{ key: 'customer' }, { key: 'protocol' }],
|
|
|
|
|
formOptions: {}, fieldsLocked: false, dirty: false,
|
|
|
|
|
sectionValue: () => ({}), searchProtocolCustomers() {}, setSectionValue() {},
|
|
|
|
|
uploadField() {}, handleFieldChange() {}, handleCustomerSelect() {}
|
|
|
|
|
}), render
|
|
|
|
|
})
|
|
|
|
|
app.mount({})
|
|
|
|
|
await Vue.nextTick()
|
|
|
|
|
const scope = {
|
|
|
|
|
projectAssignment, context, formData, mode: Vue.ref(mode),
|
|
|
|
|
submitting: Vue.ref(false), uploadPending: Vue.ref(0), isProtocolV2: Vue.ref(false),
|
|
|
|
|
isProtocolV1: Vue.ref(false), id: Vue.ref(mode === 'edit' ? 'record-1' : ''),
|
|
|
|
|
action: Vue.ref(mode), visibleSections: Vue.ref([]), dirty: Vue.ref(true),
|
|
|
|
|
showToast: (text) => messages.push(text), showSuccessToast: (text) => messages.push(text),
|
|
|
|
|
validateRequiredFields: () => '', buildMarketActionPayload: (_module, _action, data) => data,
|
|
|
|
|
sanitizeMarketPayload: (data) => data, buildPayload: () => formData.value,
|
|
|
|
|
AbortController, submitController: null, allowLeave: false,
|
|
|
|
|
saveMarketRecord: async (...args) => { requests.push(args); return { id: 'saved' } },
|
|
|
|
|
mountGuard: { isCurrent: () => true }, mountToken: 1,
|
|
|
|
|
resolveMarketSavedDetailId: () => 'saved', router: { replace: async () => {} },
|
|
|
|
|
route: { query: {} }, marketRecordNavigationQuery: () => ({}),
|
|
|
|
|
marketErrorMessage: (cause) => cause.message
|
|
|
|
|
}
|
|
|
|
|
const submit = vm.runInNewContext(`${submitSource}\nsubmit`, scope)
|
|
|
|
|
return { submit, messages, requests, app, validations: () => validations }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const role of ['specialist', 'manager']) {
|
|
|
|
|
for (const mode of ['create', 'edit']) {
|
|
|
|
|
test(`${role} ${mode}: a valid project reaches the save request`, async () => {
|
|
|
|
|
const form = await mountForm({ role, mode })
|
|
|
|
|
try {
|
|
|
|
|
await form.submit()
|
|
|
|
|
assert.equal(form.validations(), 1)
|
|
|
|
|
assert.equal(form.requests.length, 1)
|
|
|
|
|
assert.equal(form.requests[0][0].marketRole, role)
|
|
|
|
|
assert.deepEqual(form.messages, ['保存成功'])
|
|
|
|
|
} finally { form.app.unmount() }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
for (const [state, message] of [
|
|
|
|
|
[{ projectId: '' }, '请选择所属项目'],
|
|
|
|
|
[{ loading: true }, '项目选项加载中,请稍后保存'],
|
|
|
|
|
[{ error: 'network' }, '项目选项加载失败,请重试后保存']
|
|
|
|
|
]) {
|
|
|
|
|
test(`${role}: ${message}`, async () => {
|
|
|
|
|
const form = await mountForm({ role, ...state })
|
|
|
|
|
try {
|
|
|
|
|
await form.submit()
|
|
|
|
|
assert.equal(form.validations(), 1)
|
|
|
|
|
assert.equal(form.requests.length, 0)
|
|
|
|
|
assert.deepEqual(form.messages, [message])
|
|
|
|
|
} finally { form.app.unmount() }
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|