|
|
import fs from 'node:fs'
|
|
|
import assert from 'node:assert/strict'
|
|
|
const source = fs.readFileSync(
|
|
|
new URL('../common/survey.js', import.meta.url),
|
|
|
'utf8'
|
|
|
)
|
|
|
const { required, prepareQuestions, fillUserFields, visibleQuestions, selectedNotes, firstError, progress, submission, showOther, descriptionHtml } =
|
|
|
await import(
|
|
|
'data:text/javascript;base64,' + Buffer.from(source).toString('base64')
|
|
|
)
|
|
|
const questions = prepareQuestions([
|
|
|
{ id: 1, name: '资源', edit_input: 'section' },
|
|
|
{
|
|
|
id: 2,
|
|
|
name: '对接',
|
|
|
edit_input: 'multi_dimension',
|
|
|
need_fill: true,
|
|
|
dimensions: [
|
|
|
{
|
|
|
field: 'intent',
|
|
|
name: '意向',
|
|
|
need_fill: true,
|
|
|
select_item: [{ key: 'no', value: '否' }]
|
|
|
},
|
|
|
{
|
|
|
field: 'count',
|
|
|
name: '数量',
|
|
|
edit_input: 'number',
|
|
|
need_fill: true,
|
|
|
config: { integer: true, min: 0, max: 10 }
|
|
|
}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
id: 3,
|
|
|
name: '需求',
|
|
|
edit_input: 'checkbox',
|
|
|
config: { other_key: 'other', other_required: true }
|
|
|
}
|
|
|
])
|
|
|
assert.equal(progress(questions).total, 2)
|
|
|
assert.equal(firstError(questions).id, 2)
|
|
|
questions[1].value.intent = 'no'
|
|
|
assert.equal(progress(questions).answered, 0)
|
|
|
questions[1].value.count = '0'
|
|
|
assert.equal(firstError(questions), null)
|
|
|
assert.equal(progress(questions).answered, 1)
|
|
|
assert.equal(submission(questions)[0].value.count, 0)
|
|
|
questions[1].value.count = '-1'
|
|
|
assert.match(firstError(questions).message, /不能小于/)
|
|
|
questions[1].value.count = '1.5'
|
|
|
assert.match(firstError(questions).message, /整数/)
|
|
|
questions[1].value.count = '11'
|
|
|
assert.match(firstError(questions).message, /不能大于/)
|
|
|
questions[1].value.count = '0'
|
|
|
questions[2].value = ['other']
|
|
|
assert.equal(showOther(questions[2], questions[2].value), true)
|
|
|
assert.equal(firstError(questions).id, 3)
|
|
|
assert.equal(progress(questions).total, 2)
|
|
|
assert.equal(progress(questions).answered, 1)
|
|
|
questions[2].other_text = '对接银行'
|
|
|
assert.equal(firstError(questions), null)
|
|
|
assert.equal(progress(questions).percent, 100)
|
|
|
questions[2].value = []
|
|
|
assert.equal(submission(questions)[1].other_text, '')
|
|
|
assert.equal(submission(questions).length, 2)
|
|
|
questions[1].need_fill = false
|
|
|
questions[1].value = {}
|
|
|
assert.equal(firstError(questions), null)
|
|
|
questions[1].need_fill = true
|
|
|
assert.equal(firstError(questions).id, 2)
|
|
|
assert.equal(showOther({ config: { other_key: '0' } }, ['0']), true)
|
|
|
assert.equal(showOther({ config: {}, allow_input: '0' }, []), false)
|
|
|
console.log('Survey mobile behavior: 20 assertions passed')
|
|
|
|
|
|
// Exercise the real page handlers with Vue 2 reactivity (including supplement visibility).
|
|
|
const { createRequire } = await import('node:module')
|
|
|
const require = createRequire(import.meta.url)
|
|
|
const Vue = require('../../sz-kjsxy-web/node_modules/vue')
|
|
|
const pageSource = fs.readFileSync(new URL('../packages/surveyFill/index.vue', import.meta.url), 'utf8')
|
|
|
const pageScript = pageSource.split('<script>')[1].split('</script>')[0]
|
|
|
.replace(/import[\s\S]*?from\s+['"][^'"]+['"]/g, '')
|
|
|
.replace('export default', 'return')
|
|
|
const page = new Function('SurveyField', 'prepareQuestions', 'required', 'showOther', 'firstError', 'progress', 'submission', 'formatDescription', 'fillUserFields', 'visibleQuestions', 'selectedNotes', pageScript)(
|
|
|
{}, prepareQuestions, () => false, showOther, firstError, progress, submission, descriptionHtml, fillUserFields, visibleQuestions, selectedNotes
|
|
|
)
|
|
|
const vm = new Vue(page)
|
|
|
vm.questionList = prepareQuestions([{ id: 9, name: '资源', edit_input: 'checkbox', config: { other_key: 'other', other_required: true }, select_item: [{ key: 'other', value: '其他' }] }])
|
|
|
vm.setValue(vm.questionList[0], ['other'])
|
|
|
await Vue.nextTick()
|
|
|
assert.equal(vm.questionList[0].show_other, true)
|
|
|
vm.questionList[0].other_text = '补充需求'
|
|
|
assert.equal(submission(vm.questionList)[0].other_text, '补充需求')
|
|
|
vm.setValue(vm.questionList[0], [])
|
|
|
await Vue.nextTick()
|
|
|
assert.equal(vm.questionList[0].show_other, false)
|
|
|
assert.equal(vm.questionList[0].other_text, '')
|
|
|
const fieldSource = fs.readFileSync(new URL('../packages/surveyFill/components/SurveyField.vue', import.meta.url), 'utf8')
|
|
|
const field = new Function('SurveyFile', fieldSource.split('<script>')[1].split('</script>')[0].replace(/import SurveyFile[^\n]*/, '').replace('export default', 'return'))({})
|
|
|
let emitted
|
|
|
field.methods.checkboxChange.call({ $emit: (event, value) => { emitted = value } }, { detail: { value: ['other'] } })
|
|
|
assert.deepEqual(emitted, ['other'])
|
|
|
field.methods.radioChange.call({ $emit: (event, value) => { emitted = value } }, 'ten')
|
|
|
assert.equal(emitted, 'ten')
|
|
|
vm.$destroy()
|
|
|
console.log('Survey page/component events: 6 assertions passed')
|
|
|
|
|
|
const integerInput = new Vue({ ...field, propsData: { field: { edit_input: 'number', config: { integer: true, min: 0 } }, value: '12' } })
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '12abc' } }), '12')
|
|
|
await Vue.nextTick()
|
|
|
assert.equal(integerInput.numberText, '12')
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '中文' } }), '12')
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '1.2' } }), '12')
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '-2' } }), '12')
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '0' } }), '0')
|
|
|
assert.equal(integerInput.numberInput({ detail: { value: '' } }), '')
|
|
|
const decimalInput = new Vue({ ...field, propsData: { field: { edit_input: 'number', config: { integer: false } }, value: '' } })
|
|
|
assert.equal(decimalInput.numberInput({ detail: { value: '-2.5' } }), '-2.5')
|
|
|
assert.equal(decimalInput.numberInput({ detail: { value: '1.2.3' } }), '')
|
|
|
integerInput.$destroy()
|
|
|
decimalInput.$destroy()
|
|
|
console.log('Numeric input filtering: 9 assertions passed')
|
|
|
let scrollRequest
|
|
|
const query = {
|
|
|
in() { return this }, select() { return this }, selectViewport() { return this },
|
|
|
boundingClientRect() { return this }, scrollOffset() { return this },
|
|
|
exec(callback) { callback([{ height: 90 }, { top: 450 }, { scrollTop: 200 }]) }
|
|
|
}
|
|
|
globalThis.uni = { createSelectorQuery: () => query, pageScrollTo: options => { scrollRequest = options } }
|
|
|
const navigation = new Vue(page)
|
|
|
navigation.selectChapter(4)
|
|
|
assert.equal(navigation.activeChapter, 4)
|
|
|
assert.equal(scrollRequest.scrollTop, 548)
|
|
|
navigation.$destroy()
|
|
|
delete globalThis.uni
|
|
|
console.log('Chapter navigation: 2 assertions passed')
|
|
|
|
|
|
assert.equal(descriptionHtml('第一行\n第二行'), '第一行<br>第二行')
|
|
|
const html = descriptionHtml('<p style="font-size:36px;font-family:serif;color:red;text-indent:2em"><strong>说明</strong><font size="7" face="serif">内容</font></p>')
|
|
|
assert.equal(html.includes('36px'), false)
|
|
|
assert.equal(html.includes('font-family:serif'), false)
|
|
|
assert.equal(html.includes('size="7"'), false)
|
|
|
assert.equal(html.includes('color:red'), true)
|
|
|
globalThis.uni = { getSystemInfoSync: () => ({ windowWidth: 375, platform: 'devtools' }) }
|
|
|
globalThis.wx = { canIUse: () => false }
|
|
|
const Parser = require('../uview-ui/components/u-parse/libs/MpHtmlParser.js')
|
|
|
const parsed = new Parser(html, { nodes: [] }).parse()
|
|
|
assert.ok(parsed.length > 0)
|
|
|
assert.match(parsed[0].attrs.style, /font-size:14px/)
|
|
|
assert.match(parsed[0].attrs.style, /text-indent:2em/)
|
|
|
assert.ok(pageSource.includes(':html="descriptionHtml"'))
|
|
|
delete globalThis.uni
|
|
|
delete globalThis.wx
|
|
|
console.log('Description rendering and font normalization: 9 assertions passed')
|
|
|
|
|
|
const courseSource = fs.readFileSync(new URL('../packages/mycourse/index.vue', import.meta.url), 'utf8')
|
|
|
assert.ok(!courseSource.includes('问卷调查'))
|
|
|
assert.ok(!courseSource.includes('/packages/surveyFill'))
|
|
|
console.log('Course survey entry removed')
|
|
|
|
|
|
const fullSurvey = prepareQuestions([
|
|
|
...Array.from({ length: 6 }, (_, i) => ({ id: 'section' + i, edit_input: 'section' })),
|
|
|
...Array.from({ length: 28 }, (_, i) => ({ id: i, edit_input: 'multi_dimension',
|
|
|
dimensions: Array.from({ length: i < 8 ? 3 : 2 }, (_, j) => ({ field: 'd' + j, edit_input: 'number' })) }))
|
|
|
])
|
|
|
assert.equal(fullSurvey.filter(q => q.edit_input !== 'section').reduce((sum, q) => sum + q.dimensions.length, 0), 64)
|
|
|
assert.equal(progress(fullSurvey).total, 28)
|
|
|
console.log('Question count uses parent questions, excluding chapters and supplementary fields')
|
|
|
|
|
|
const savedQuestions = [
|
|
|
{ id: 1, edit_input: 'multi_dimension', value: { n: 0, r: '旧标签' }, dimensions: [
|
|
|
{ field: 'n', edit_input: 'number' }, { field: 'r', selected_keys: ['no'] }] },
|
|
|
{ id: 2, edit_input: 'checkbox', selected_keys: ['other'], other_text: '补充内容', config: { other_key: 'other' } },
|
|
|
{ id: 3, edit_input: 'radio', selected_keys: ['0'] },
|
|
|
{ id: 4, edit_input: 'textarea', value: '原建议' }
|
|
|
]
|
|
|
let toast
|
|
|
const previousUni = globalThis.uni
|
|
|
globalThis.uni = { showToast: args => { toast = args.title } }
|
|
|
const completeProfile = { mobile: '13800000000', name: '测试学员', sex: '男', birthday: '1990-01-01', email: 'test@example.com', company_name: '测试公司', company_position: '经理' }
|
|
|
const returned = new Vue(page)
|
|
|
returned.$u = { api: { user: async () => ({ user: completeProfile }), courseEvaluationDetail: async () => ({ id: 8, title: '新标题', my_response: {
|
|
|
title: '提交时标题', desc: '<p>原说明</p>', questions: savedQuestions
|
|
|
} }) } }
|
|
|
await returned.load()
|
|
|
assert.equal(toast, '您已提交,可修改后重新提交')
|
|
|
assert.equal(returned.completed, true)
|
|
|
assert.equal(returned.survey.title, '提交时标题')
|
|
|
assert.equal(returned.questionList[0].value.n, 0)
|
|
|
assert.equal(returned.questionList[0].value.r, 'no')
|
|
|
assert.deepEqual([...returned.questionList[1].value], ['other'])
|
|
|
assert.equal(returned.questionList[1].show_other, true)
|
|
|
assert.equal(returned.questionList[1].other_text, '补充内容')
|
|
|
assert.equal(returned.questionList[2].value, '0')
|
|
|
assert.equal(returned.questionList[3].value, '原建议')
|
|
|
returned.setValue(returned.questionList[2], 'changed')
|
|
|
returned.setDimension(returned.questionList[0], { field: 'n' }, 5)
|
|
|
assert.equal(returned.questionList[2].value, 'changed')
|
|
|
assert.equal(returned.questionList[0].value.n, 5)
|
|
|
let confirmation
|
|
|
globalThis.uni.showModal = args => { confirmation = args.content; args.success({ confirm: false }) }
|
|
|
returned.submit()
|
|
|
assert.equal(confirmation, '确认重新提交?将更新之前的答案。')
|
|
|
globalThis.uni = previousUni
|
|
|
returned.$destroy()
|
|
|
console.log('Submitted survey reentry: snapshot restored, toast shown and editing and resubmission enabled')
|
|
|
|
|
|
const prefills = prepareQuestions([
|
|
|
{ id: 1, edit_input: 'text', config: { user_field: 'name' } },
|
|
|
{ id: 2, edit_input: 'text', config: { user_field: 'mobile' } },
|
|
|
{ id: 3, edit_input: 'text', config: { user_field: 'email' } },
|
|
|
{ id: 4, edit_input: 'text', config: { user_field: 'password' } }
|
|
|
])
|
|
|
fillUserFields(prefills, { name: '张三', mobile: '13800000000', password: 'secret' })
|
|
|
assert.equal(prefills[0].value, '张三')
|
|
|
assert.equal(prefills[1].value, '13800000000')
|
|
|
assert.equal(prefills[2].value, '')
|
|
|
assert.equal(prefills[3].value, '')
|
|
|
prefills[0].value = '自填姓名'
|
|
|
fillUserFields(prefills, { name: '张三' })
|
|
|
assert.equal(prefills[0].value, '自填姓名')
|
|
|
const gate = new Vue(page)
|
|
|
gate.id = '8'
|
|
|
gate.$u = { api: { user: async () => ({ user: {} }), courseEvaluationDetail: async () => ({ id: 8, course_content_evaluation_asks: [] }) } }
|
|
|
await gate.load()
|
|
|
assert.equal(gate.profileRequired, true)
|
|
|
assert.equal(gate.showRegister, true)
|
|
|
gate.submit() // no modal or API available; must be blocked
|
|
|
let destination
|
|
|
globalThis.uni = { redirectTo: args => { destination = args.url } }
|
|
|
gate.goIdentity('login')
|
|
|
assert.equal(destination, '/packages/register/login?survey_id=8')
|
|
|
gate.goIdentity('index')
|
|
|
assert.equal(destination, '/packages/register/index?survey_id=8')
|
|
|
globalThis.uni = previousUni
|
|
|
gate.$destroy()
|
|
|
console.log('Profile prefill, whitelist and identity gate passed')
|
|
|
|
|
|
const navigationSource = fs.readFileSync(new URL('../common/surveyNavigation.js', import.meta.url), 'utf8')
|
|
|
const { returnToSurvey } = await import('data:text/javascript;base64,' + Buffer.from(navigationSource).toString('base64'))
|
|
|
|
|
|
// Execute the actual binding/registration completion handlers with mocked APIs.
|
|
|
function identityPage(name) {
|
|
|
const text = fs.readFileSync(new URL('../packages/register/' + name + '.vue', import.meta.url), 'utf8')
|
|
|
const script = text.split('<script>')[1].split('</script>')[0]
|
|
|
.replace(/import[^\n]*from[^\n]*/g, '').replace('export default', 'return')
|
|
|
return new Function('CompanySelector', 'returnToSurvey', script)({}, returnToSurvey)
|
|
|
}
|
|
|
const bindingPage = identityPage('login')
|
|
|
const registrationPage = identityPage('index')
|
|
|
let identityRoute
|
|
|
const noop = () => {}
|
|
|
globalThis.uni = { redirectTo: args => { identityRoute = args.url; if (args.success) args.success() }, switchTab: args => { identityRoute = args.url }, removeStorageSync: noop, setStorageSync: noop }
|
|
|
const binding = {
|
|
|
survey_id: '', form: { mobile: '13800000000', code: '123456' }, sendTimer: null,
|
|
|
base: { isNull: value => !value, toast: noop },
|
|
|
$u: { vuex: noop, api: { checkMobile: async () => ({ token: 'test-token' }) } }
|
|
|
}
|
|
|
bindingPage.onLoad.call(binding, { survey_id: '8' })
|
|
|
bindingPage.methods.toRegister.call(binding)
|
|
|
assert.equal(identityRoute, '/packages/register/index?survey_id=8')
|
|
|
bindingPage.methods.handleMsgLogin.call(binding)
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=8')
|
|
|
bindingPage.onLoad.call(binding, {})
|
|
|
bindingPage.methods.handleMsgLogin.call(binding)
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(identityRoute, '/pages/me/index')
|
|
|
const registration = {
|
|
|
form: { username: '张三' },
|
|
|
$refs: { uForm: { validate: callback => callback(true) } },
|
|
|
base: { toast: noop },
|
|
|
$u: { vuex: noop, api: { saveUser: async () => ({}), user: async () => ({ user: completeProfile }) } }
|
|
|
}
|
|
|
registrationPage.onLoad.call(registration, { survey_id: '8' })
|
|
|
registrationPage.methods.saveUser.call(registration)
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=8')
|
|
|
registrationPage.onLoad.call(registration, { survey_id: 'https://invalid.example' })
|
|
|
assert.equal(registration.survey_id, '')
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Binding and registration callbacks return to the original survey; ordinary login retains its original destination')
|
|
|
const conditional = prepareQuestions([
|
|
|
{ id: 1, field: 'insurance', edit_input: 'radio' },
|
|
|
{ id: 2, field: 'identity', name: '身份证', edit_input: 'text', need_fill: true, config: { visible_when: { field: 'insurance', value: 'yes' } } },
|
|
|
{ id: 3, field: 'venues', name: '会场', edit_input: 'checkbox', config: { max_choices: 2, option_notes: { tax: { placeholder: '咨询问题' } } }, select_item: [{ key: 'tax', value: '财税' }, { key: 'legal', value: '法律' }, { key: 'ip', value: '知识产权' }] }
|
|
|
])
|
|
|
assert.equal(visibleQuestions(conditional).length, 2)
|
|
|
conditional[1].value = '预填证件'
|
|
|
assert.equal(submission(conditional).some(q => q.id === 2), false)
|
|
|
conditional[0].value = 'yes'
|
|
|
assert.equal(visibleQuestions(conditional).length, 3)
|
|
|
conditional[1].value = ''
|
|
|
assert.equal(firstError(conditional).id, 2)
|
|
|
conditional[1].value = '填写证件'
|
|
|
conditional[2].value = ['tax', 'legal', 'ip']
|
|
|
assert.match(firstError(conditional).message, /最多选择2项/)
|
|
|
conditional[2].value = ['tax']
|
|
|
conditional[2].option_texts = { tax: '财税问题', legal: '未选不提交' }
|
|
|
assert.equal(selectedNotes(conditional[2]).length, 1)
|
|
|
assert.deepEqual(submission(conditional)[2].option_texts, { tax: '财税问题' })
|
|
|
console.log('Conditional visibility, selection limits and per-option supplement submission passed')
|
|
|
|
|
|
// The first successful registration must navigate even when toast never calls back
|
|
|
// and profile refresh would fail. Concurrent clicks must not save twice.
|
|
|
let completeSave
|
|
|
let saves = 0
|
|
|
let profileReads = 0
|
|
|
const freshRegistration = {
|
|
|
survey_id: '9', saving: false, registrationComplete: false, form: { username: '新用户' },
|
|
|
$refs: { uForm: { validate: callback => callback(true) } },
|
|
|
base: { toast: noop },
|
|
|
$u: { vuex: noop, api: {
|
|
|
saveUser: () => { saves++; return new Promise(resolve => { completeSave = resolve }) },
|
|
|
user: () => { profileReads++; return Promise.reject(new Error('profile unavailable')) }
|
|
|
} }
|
|
|
}
|
|
|
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() } }
|
|
|
identityRoute = ''
|
|
|
const firstRegistration = registrationPage.methods.saveUser.call(freshRegistration)
|
|
|
registrationPage.methods.saveUser.call(freshRegistration)
|
|
|
await Promise.resolve()
|
|
|
assert.equal(saves, 1)
|
|
|
assert.equal(identityRoute, '')
|
|
|
completeSave('更新成功')
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
|
|
|
assert.equal(profileReads, 0)
|
|
|
assert.equal(freshRegistration.saving, false)
|
|
|
let fallbackRoute
|
|
|
uni.redirectTo = args => args.fail()
|
|
|
uni.reLaunch = args => { fallbackRoute = args.url; args.success() }
|
|
|
await returnToSurvey('9')
|
|
|
assert.equal(fallbackRoute, '/packages/surveyFill/index?id=9')
|
|
|
uni.reLaunch = args => args.fail(new Error('navigation failed'))
|
|
|
await assert.rejects(returnToSurvey('9'))
|
|
|
await registrationPage.methods.saveUser.call(freshRegistration)
|
|
|
assert.equal(saves, 1) // Retry navigation only, never save again.
|
|
|
assert.equal(freshRegistration.saving, false)
|
|
|
let bindingRequests = 0
|
|
|
const failedBinding = { ...binding, binding: false, bindingComplete: false, survey_id: '9', $u: { vuex: noop, api: { checkMobile: async () => { bindingRequests++; throw new Error('invalid code') } } } }
|
|
|
identityRoute = ''
|
|
|
await bindingPage.methods.handleMsgLogin.call(failedBinding)
|
|
|
assert.equal(bindingRequests, 1)
|
|
|
assert.equal(identityRoute, '')
|
|
|
assert.equal(failedBinding.binding, false)
|
|
|
assert.equal(failedBinding.bindingComplete, false)
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('First registration navigates without toast/profile callbacks; duplicate clicks, navigation fallback/retry and binding failures verified')
|
|
|
|
|
|
const chained = [
|
|
|
{ id: 91, field: 'first', value: 'no' },
|
|
|
{ id: 92, field: 'second', value: 'yes', config: { visible_when: { field: 'first', value: 'yes' } } },
|
|
|
{ id: 93, field: 'third', value: 'old', config: { visible_when: { field: 'second', value: 'yes' } } }
|
|
|
]
|
|
|
assert.deepEqual(visibleQuestions(chained).map(q => q.id), [91])
|
|
|
assert.deepEqual(submission(chained).map(q => q.id), [91])
|
|
|
chained[0].value = 'yes'
|
|
|
assert.equal(visibleQuestions(chained).length, 3)
|
|
|
|
|
|
for (const [state, message] of [
|
|
|
[undefined, '您还未报名相关课程'],
|
|
|
['not_started', '该问卷尚未开始,请于2026年10月23日后扫码打开该问卷。'],
|
|
|
['ended', '该问卷已结束。'],
|
|
|
['draft', '该问卷还未开始。']
|
|
|
]) {
|
|
|
const denied = new Vue(page)
|
|
|
denied.id = 9
|
|
|
denied.questionList = prepareQuestions([{ id: 1, name: '旧问题', edit_input: 'text' }])
|
|
|
denied.survey = { title: '旧问卷' }
|
|
|
denied.showRegister = true
|
|
|
let calls = 0
|
|
|
denied.$u = { api: { courseEvaluationDetail: async () => { calls++; return { id: 9, access_state: state, access_error: message } },
|
|
|
user: () => { throw new Error('must not fetch profile') } } }
|
|
|
globalThis.uni = { showToast: () => { throw new Error('must show message on page') },
|
|
|
switchTab: () => { throw new Error('must stay on survey page') },
|
|
|
showModal: () => { throw new Error('must not submit unavailable survey') } }
|
|
|
await denied.load()
|
|
|
assert.equal(denied.unavailableMessage, message)
|
|
|
assert.equal(denied.error, '')
|
|
|
assert.equal(denied.loading, false)
|
|
|
assert.equal(denied.showRegister, false)
|
|
|
assert.deepEqual(denied.questionList, [])
|
|
|
assert.deepEqual(denied.survey, {})
|
|
|
denied.submit()
|
|
|
await denied.load()
|
|
|
assert.equal(calls, 2)
|
|
|
denied.$u.api.courseEvaluationDetail = async () => ({ id: 9, title: '已开始问卷', course_content_evaluation_asks: [{ id: 1, edit_input: 'text', name: '姓名' }] })
|
|
|
denied.$u.api.user = async () => ({ user: { mobile: '13800000000' } })
|
|
|
await denied.load()
|
|
|
assert.equal(denied.unavailableMessage, '')
|
|
|
assert.equal(denied.survey.title, '已开始问卷')
|
|
|
assert.equal(denied.questionList.length, 1)
|
|
|
denied.$destroy()
|
|
|
}
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Draft, future and ended surveys hide stale content, stay on page, block submit and can reload after opening')
|
|
|
|
|
|
// Access may be revoked after opening the form: submission must hide the old
|
|
|
// questions as well, without a toast, delayed navigation or registration popup.
|
|
|
const audienceRevoked = new Vue(page)
|
|
|
audienceRevoked.id = 9
|
|
|
audienceRevoked.survey = { title: '此前可填写', schema_hash: 'test' }
|
|
|
audienceRevoked.questionList = prepareQuestions([{ id: 1, name: '意见', edit_input: 'text' }])
|
|
|
audienceRevoked.questionList[0].value = '已填写内容'
|
|
|
audienceRevoked.$u = { api: { courseContentForm: async () => ({ access_error: '您还未报名相关课程' }) } }
|
|
|
let revokedSubmission
|
|
|
globalThis.uni = {
|
|
|
showModal: args => { revokedSubmission = args.success({ confirm: true }) },
|
|
|
showToast: () => { throw new Error('must display the audience message on page') },
|
|
|
switchTab: () => { throw new Error('must not return home') }
|
|
|
}
|
|
|
audienceRevoked.submit()
|
|
|
await revokedSubmission
|
|
|
assert.equal(audienceRevoked.unavailableMessage, '您还未报名相关课程')
|
|
|
assert.deepEqual(audienceRevoked.questionList, [])
|
|
|
assert.deepEqual(audienceRevoked.survey, {})
|
|
|
assert.equal(audienceRevoked.submitting, false)
|
|
|
assert.equal(audienceRevoked.showRegister, false)
|
|
|
audienceRevoked.$destroy()
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Audience denial at entry and submission hides questions, stays on page and supports reloading after approval')
|
|
|
|
|
|
const actualNow = Date.now
|
|
|
Date.now = () => Date.UTC(2026, 9, 22, 4, 0, 0) // 2026-10-22 12:00:00 Asia/Shanghai
|
|
|
try {
|
|
|
for (const [fields, message] of [
|
|
|
[{ status: '0', start_time: '2026-10-26 00:00:00' }, '该问卷尚未开始,请于2026年10月26日后扫码打开该问卷。'],
|
|
|
[{ status: 0 }, '该问卷还未开始。'],
|
|
|
[{ status: '0', start_time: '2026-10-26 09:30:15' }, '该问卷尚未开始,请于2026年10月26日 09:30:15后扫码打开该问卷。'],
|
|
|
[{ status: '0', start_time: '2026-10-21 00:00:00' }, '该问卷还未开始。'],
|
|
|
[{ status: '1', start_time: '2026-10-26 00:00:00' }, '该问卷尚未开始,请于2026年10月26日后扫码打开该问卷。'],
|
|
|
[{ status: 1, start_time: '2026-10-26 09:30:15' }, '该问卷尚未开始,请于2026年10月26日 09:30:15后扫码打开该问卷。'],
|
|
|
[{ status: 1, end_time: '2026-10-22 12:00:00' }, '该问卷已结束。'],
|
|
|
[{ access_error: '当前问卷已截止' }, '该问卷已结束。']
|
|
|
]) {
|
|
|
const denied = new Vue(page)
|
|
|
denied.id = 9
|
|
|
denied.$u = { api: { courseEvaluationDetail: async () => ({ id: 9, title: '不应显示', course_content_evaluation_asks: [{ id: 1, name: '不应显示的题目', edit_input: 'text' }], ...fields }),
|
|
|
user: () => { throw new Error('must not fetch profile before checking raw status and dates') } } }
|
|
|
globalThis.uni = { showToast: () => { throw new Error('must stay on message page') }, switchTab: () => { throw new Error('must not leave page') } }
|
|
|
await denied.load()
|
|
|
assert.equal(denied.unavailableMessage, message)
|
|
|
assert.equal(denied.error, '')
|
|
|
assert.deepEqual(denied.questionList, [])
|
|
|
assert.deepEqual(denied.survey, {})
|
|
|
denied.$destroy()
|
|
|
}
|
|
|
const available = new Vue(page)
|
|
|
assert.equal(available.availabilityError({ status: '1', start_time: '2026-10-22 12:00:00', end_time: '2026-10-23 00:00:00' }), null)
|
|
|
assert.equal(available.availabilityError({ status: '1', start_time: null, end_time: null }), null)
|
|
|
available.$destroy()
|
|
|
} finally {
|
|
|
Date.now = actualNow
|
|
|
globalThis.uni = previousUni
|
|
|
}
|
|
|
console.log('Legacy full responses: string draft status, future opening, deadline and Shanghai timezone boundaries are blocked before profile/questions load')
|
|
|
|
|
|
const fileQuestions = prepareQuestions([{ id: 301, edit_input: 'file', name: '文件', need_fill: true, config: { max_files: 1 } }])
|
|
|
assert.ok(firstError(fileQuestions))
|
|
|
fileQuestions[0].value = [{ id: 81, original_name: '材料.pdf' }]
|
|
|
assert.equal(firstError(fileQuestions), null)
|
|
|
assert.equal(submission(fileQuestions)[0].value[0].id, 81)
|
|
|
assert.equal(prepareQuestions(fileQuestions, true)[0].value[0].original_name, '材料.pdf')
|
|
|
fileQuestions[0].value.push({ id: 82 })
|
|
|
assert.ok(firstError(fileQuestions))
|
|
|
const uploadSource = fs.readFileSync(new URL('../packages/surveyFill/components/SurveyFile.vue', import.meta.url), 'utf8')
|
|
|
const uploadComponent = new Function('ROOTPATH', uploadSource.split('<script>')[1].split('</script>')[0].replace(/import[^\n]*/, '').replace('export default', 'return'))('https://example.invalid')
|
|
|
const uploader = new Vue({ ...uploadComponent, propsData: { value: [], limit: 2 } })
|
|
|
const events = []
|
|
|
uploader.$on('input', value => events.push(value))
|
|
|
const states = []
|
|
|
uploader.$on('uploading', value => states.push(value))
|
|
|
globalThis.uni = { getStorageSync: () => ({ vuex_token: 'test' }), uploadFile: options => options.success({ statusCode: 200, data: JSON.stringify({ id: 81, url: '/file.pdf', original_name: options.formData.original_name }) }), showToast: () => {} }
|
|
|
await uploader.upload([{ path: '/tmp/a.pdf', name: '材料.pdf', size: 100 }])
|
|
|
assert.equal(events[0][0].original_name, '材料.pdf')
|
|
|
assert.deepEqual(states, [true, false])
|
|
|
uni.uploadFile = options => options.fail()
|
|
|
await uploader.upload([{ path: '/tmp/b.pdf', name: 'b.pdf', size: 100 }])
|
|
|
assert.equal(events.length, 1)
|
|
|
assert.equal(uploader.uploading, false)
|
|
|
uploader.$destroy()
|
|
|
console.log('File answers: required, limits, restore, upload success and failure verified')
|
|
|
|
|
|
const restricted = new Vue({ ...uploadComponent, propsData: { value: [], limit: 2, extensions: ['pdf'], questionId: 301 } })
|
|
|
let uploads = 0, lastNotice = ''
|
|
|
globalThis.uni = { getStorageSync: () => ({ vuex_token: 'test' }), showToast: o => { lastNotice = o.title }, uploadFile: o => {
|
|
|
uploads++
|
|
|
assert.equal(o.formData.survey_question_id, 301)
|
|
|
o.success({ statusCode: 200, data: JSON.stringify({ id: 91, url: '/a.pdf' }) })
|
|
|
} }
|
|
|
await restricted.upload([{ path: '/tmp/no.docx', name: 'no.docx', size: 10 }])
|
|
|
assert.equal(uploads, 0)
|
|
|
assert.ok(lastNotice.includes('pdf'))
|
|
|
await restricted.upload([{ path: '/tmp/yes.PDF', name: 'yes.PDF', size: 10 }])
|
|
|
assert.equal(uploads, 1)
|
|
|
restricted.$destroy()
|
|
|
console.log('Configured upload formats reject unsupported files and allow uppercase suffixes')
|
|
|
|
|
|
assert.ok(!fieldSource.split('<script>')[0].includes('|| {}'))
|
|
|
assert.equal(field.computed.fileLimit.call({ field: {} }), 1)
|
|
|
assert.equal(field.computed.fileExtensions.call({ field: { config: { allowed_extensions: ['pdf'] } } })[0], 'pdf')
|
|
|
console.log('Upload template uses computed bindings without WXML object literals')
|
|
|
|
|
|
assert.ok(pageSource.includes('q.help && q.edit_input !=='))
|
|
|
assert.ok(pageSource.includes('q.config.max_choices > 0'))
|
|
|
assert.ok(!uploadSource.split('<script>')[0].includes('点击查看'))
|
|
|
assert.ok(!uploadSource.split('<script>')[0].includes('单文件小于'))
|
|
|
assert.ok(!uploadSource.split('<script>')[0].includes('@click="open(file)"'))
|
|
|
|
|
|
for (const help of ['请简要说明', '', ' ']) {
|
|
|
assert.equal(field.computed.textPlaceholder.call({ field: { help } }), help.trim() || '请输入')
|
|
|
}
|
|
|
assert.ok(pageSource.includes("q.edit_input !== 'text' && q.edit_input !== 'textarea'"))
|
|
|
|
|
|
const financing = prepareQuestions([
|
|
|
{ id: 601, name: '融资', field: 'finance', edit_input: 'radio' },
|
|
|
{ id: 602, name: '材料', field: 'material', edit_input: 'file', need_fill: true, rule: 'required', config: { visible_when: { field: 'finance', values: ['later', 'road'], required_values: ['road'] } } }
|
|
|
])
|
|
|
financing[0].value = 'later'
|
|
|
assert.equal(visibleQuestions(financing).length, 2)
|
|
|
assert.equal(required(financing[1], financing), false)
|
|
|
assert.equal(firstError(financing), null)
|
|
|
financing[0].value = 'road'
|
|
|
assert.equal(required(financing[1], financing), true)
|
|
|
assert.equal(firstError(financing).id, 602)
|
|
|
financing[0].value = 'no'
|
|
|
assert.equal(visibleQuestions(financing).length, 1)
|
|
|
assert.equal(firstError(financing), null)
|
|
|
assert.equal(submission(financing).length, 1)
|
|
|
financing[0].edit_input = 'checkbox'
|
|
|
financing[0].value = ['later', 'road']
|
|
|
assert.equal(required(financing[1], financing), true)
|
|
|
financing[0].value = ['later']
|
|
|
assert.equal(required(financing[1], financing), false)
|
|
|
console.log('Multiple visibility triggers and required overrides verified')
|
|
|
|
|
|
const incomplete = new Vue(page)
|
|
|
incomplete.$u = { api: { user: async () => ({ user: { mobile: '13800000000' } }), courseEvaluationDetail: async () => ({ id: 9, requires_profile: true }) } }
|
|
|
await incomplete.load()
|
|
|
assert.equal(incomplete.profileRequired, false)
|
|
|
assert.equal(incomplete.showRegister, false)
|
|
|
incomplete.$destroy()
|
|
|
identityRoute = ''
|
|
|
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() }, removeStorageSync: noop, setStorageSync: noop }
|
|
|
const needsProfile = { ...binding, binding: false, bindingComplete: false, survey_id: '9', $u: { vuex: noop, api: { checkMobile: async () => ({ token: 'test', requires_profile: true }) } } }
|
|
|
await bindingPage.methods.handleMsgLogin.call(needsProfile)
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
|
|
|
const registrationPrefill = { form: {}, survey_id: '', base: { toast: noop }, $u: { api: { user: async () => ({ user: completeProfile }) } } }
|
|
|
registrationPage.onLoad.call(registrationPrefill, { survey_id: '9' })
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(registrationPrefill.form.username, completeProfile.name)
|
|
|
assert.equal(registrationPrefill.form.mobile, completeProfile.mobile)
|
|
|
assert.equal(registrationPrefill.form.company_name, completeProfile.company_name)
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Mobile-only survey gate, binding return and verified profile prefill passed')
|
|
|
|
|
|
let automaticSaves = 0
|
|
|
identityRoute = ''
|
|
|
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() } }
|
|
|
const automaticRegistration = {
|
|
|
survey_id: '9', form: { ...completeProfile, username: completeProfile.name }, myMobile: completeProfile.mobile, myCode: '123456',
|
|
|
saving: false, bindingMobile: false, registrationComplete: false, isBasicInfoIncomplete: () => false,
|
|
|
$refs: { uForm: { validate: callback => callback(true) } },
|
|
|
base: { toast: noop, isNull: value => !value },
|
|
|
$u: { api: { bindMobile: async () => ({ id: 200, mobile: completeProfile.mobile }), saveUser: async payload => { automaticSaves++; assert.equal(payload.survey_id, '9'); return '更新成功' } } },
|
|
|
$set: (target, key, value) => { target[key] = value }, $nextTick: () => Promise.resolve(),
|
|
|
saveUser(options) { return registrationPage.methods.saveUser.call(this, options) }
|
|
|
}
|
|
|
await registrationPage.methods.changeMobile.call(automaticRegistration)
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(automaticSaves, 1)
|
|
|
assert.equal(automaticRegistration.registrationComplete, true)
|
|
|
assert.equal(identityRoute, '')
|
|
|
automaticRegistration.form.company_position = '修改后职务'
|
|
|
automaticRegistration.$u.api.saveUser = async payload => {
|
|
|
automaticSaves++
|
|
|
assert.equal(payload.company_position, '修改后职务')
|
|
|
return '更新成功'
|
|
|
}
|
|
|
assert.equal(await automaticRegistration.saveUser(), true)
|
|
|
assert.equal(automaticSaves, 2)
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Auto-registration stays on the page; explicit registration saves edits before returning')
|
|
|
|
|
|
// Simulate the u-form child receiving its phone prop only on Vue's next update.
|
|
|
let boundRequests = 0, registeredRequests = 0, finishRegistration
|
|
|
const reactiveRegistration = new Vue(registrationPage)
|
|
|
reactiveRegistration.form = { ...reactiveRegistration.form, ...completeProfile, username: completeProfile.name, mobile: '' }
|
|
|
reactiveRegistration.survey_id = '9'
|
|
|
reactiveRegistration.myMobile = completeProfile.mobile
|
|
|
reactiveRegistration.myCode = '123456'
|
|
|
reactiveRegistration.base = { isNull: value => value == null || !String(value).trim(), toast: noop }
|
|
|
let renderedMobile = ''
|
|
|
reactiveRegistration.$watch(() => reactiveRegistration.form.mobile, value => { renderedMobile = value })
|
|
|
reactiveRegistration.$refs.uForm = { validate: callback => {
|
|
|
assert.equal(renderedMobile, completeProfile.mobile, 'the bound phone must reach the form before validation')
|
|
|
callback(true)
|
|
|
} }
|
|
|
reactiveRegistration.$u = { api: {
|
|
|
bindMobile: async () => { boundRequests++; return { id: 200, mobile: completeProfile.mobile } },
|
|
|
saveUser: payload => {
|
|
|
registeredRequests++
|
|
|
assert.equal(payload.mobile, completeProfile.mobile)
|
|
|
assert.equal(payload.survey_id, '9')
|
|
|
return new Promise(resolve => { finishRegistration = resolve })
|
|
|
}
|
|
|
} }
|
|
|
identityRoute = ''
|
|
|
globalThis.uni = { redirectTo: options => { identityRoute = options.url; options.success() } }
|
|
|
const registrationChain = reactiveRegistration.changeMobile()
|
|
|
reactiveRegistration.changeMobile()
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(boundRequests, 1)
|
|
|
assert.equal(registeredRequests, 1)
|
|
|
assert.equal(identityRoute, '', 'must wait for the registration API to succeed')
|
|
|
assert.equal(reactiveRegistration.saving, true)
|
|
|
reactiveRegistration.changeMobile()
|
|
|
assert.equal(boundRequests, 1)
|
|
|
finishRegistration('更新成功')
|
|
|
assert.equal(await registrationChain, true)
|
|
|
assert.equal(identityRoute, '')
|
|
|
reactiveRegistration.form.username = '修改后的姓名'
|
|
|
const explicitRegistration = reactiveRegistration.saveUser()
|
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
|
assert.equal(registeredRequests, 2)
|
|
|
assert.equal(identityRoute, '')
|
|
|
finishRegistration('更新成功')
|
|
|
assert.equal(await explicitRegistration, true)
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
|
|
|
assert.equal(reactiveRegistration.saving, false)
|
|
|
assert.equal(reactiveRegistration.bindingMobile, false)
|
|
|
reactiveRegistration.$destroy()
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Reactive phone sync and the full bind/register/return promise chain verified')
|
|
|
|
|
|
// Saving or validation failures must not navigate; retry saving uses the verified phone.
|
|
|
const failedRegistration = {
|
|
|
...automaticRegistration, registrationComplete: false, saving: false, bindingMobile: false, myCode: '123456',
|
|
|
form: { ...automaticRegistration.form },
|
|
|
$u: { api: { bindMobile: async () => ({ id: 200 }), saveUser: async () => false } }
|
|
|
}
|
|
|
identityRoute = ''
|
|
|
globalThis.uni = { redirectTo: options => { identityRoute = options.url; options.success() } }
|
|
|
assert.equal(await registrationPage.methods.changeMobile.call(failedRegistration), false)
|
|
|
assert.equal(failedRegistration.registrationComplete, false)
|
|
|
assert.equal(failedRegistration.saving, false)
|
|
|
assert.equal(identityRoute, '')
|
|
|
failedRegistration.$u.api.saveUser = async () => '更新成功'
|
|
|
assert.equal(await registrationPage.methods.saveUser.call(failedRegistration), true)
|
|
|
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
|
|
|
let invalidSaves = 0
|
|
|
const invalidRegistration = {
|
|
|
...failedRegistration, registrationComplete: false, saving: false, myCode: '123456',
|
|
|
$refs: { uForm: { validate: callback => callback(false) } },
|
|
|
$u: { api: { bindMobile: async () => ({ id: 200 }), saveUser: async () => { invalidSaves++; return '更新成功' } } }
|
|
|
}
|
|
|
identityRoute = ''
|
|
|
assert.equal(await registrationPage.methods.changeMobile.call(invalidRegistration), false)
|
|
|
assert.equal(invalidSaves, 0)
|
|
|
assert.equal(identityRoute, '')
|
|
|
globalThis.uni = previousUni
|
|
|
console.log('Registration failure/validation failure never navigate; registration can retry without rebinding')
|