You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
489 lines
17 KiB
489 lines
17 KiB
import axios from 'axios'
|
|
import Attachments from '@/views/survey/components/SurveyAttachments.vue'
|
|
import { saveAs } from 'file-saver'
|
|
jest.mock('file-saver', () => ({ saveAs: jest.fn() }))
|
|
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
|
|
import ElementUI from 'element-ui'
|
|
import FieldSettings from '@/views/survey/components/SurveyFieldSettings.vue'
|
|
import Editor from '@/views/survey/components/editPane.vue'
|
|
import Question from '@/views/survey/components/SurveyQuestion.vue'
|
|
import PreviewField from '@/views/survey/components/SurveyPreviewField.vue'
|
|
import Description from '@/views/survey/components/SurveyDescription.vue'
|
|
import Fill from '@/views/survey/components/SurveyFillDialog.vue'
|
|
import Wizard from '@/views/survey/components/SurveyCreateWizard.vue'
|
|
import Results from '@/views/survey/components/SurveyResultsDialog.vue'
|
|
import { save, index as questionIndex } from '@/api/survey/index'
|
|
import { statistics, show } from '@/api/survey/evaluationForm'
|
|
jest.mock('@/api/survey/index', () => ({
|
|
save: jest.fn(() => Promise.resolve({ id: 1 })),
|
|
index: jest.fn()
|
|
}))
|
|
jest.mock('@/api/survey/evaluationForm', () => ({
|
|
statistics: jest.fn(),
|
|
index: jest.fn(),
|
|
show: jest.fn(),
|
|
textAnswers: jest.fn()
|
|
}))
|
|
const localVue = createLocalVue()
|
|
localVue.use(ElementUI)
|
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
describe('Survey editing and results', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
it('keeps unrestricted and multiple audience courses mutually exclusive', () => {
|
|
const state = { form: { audience_course_ids: [] }}
|
|
Object.defineProperty(state, 'audienceSelection', {
|
|
get: () => Wizard.computed.audienceSelection.call(state)
|
|
})
|
|
expect(state.audienceSelection).toEqual(['all'])
|
|
Wizard.methods.changeAudience.call(state, ['all', 11])
|
|
expect(state.form.audience_course_ids).toEqual([11])
|
|
Wizard.methods.changeAudience.call(state, [11, 12])
|
|
expect(state.form.audience_course_ids).toEqual([11, 12])
|
|
Wizard.methods.changeAudience.call(state, [11, 12, 'all'])
|
|
expect(state.form.audience_course_ids).toEqual([])
|
|
Wizard.methods.changeAudience.call(state, [])
|
|
expect(state.audienceSelection).toEqual(['all'])
|
|
})
|
|
it('saves mixed dimensions with zero lower bound and stable option keys', async() => {
|
|
const wrapper = shallowMount(Editor, {
|
|
localVue,
|
|
propsData: {
|
|
question: {
|
|
name: '资源对接',
|
|
field: 'resource',
|
|
edit_input: 'multi_dimension',
|
|
dimensions: [
|
|
{
|
|
name: '是否跟进',
|
|
field: 'follow',
|
|
select_item: [{ key: 'yes', value: '是' }],
|
|
need_fill: true
|
|
},
|
|
{
|
|
name: '意向数量',
|
|
field: 'count',
|
|
edit_input: 'number',
|
|
config: { integer: true, min: 0 },
|
|
need_fill: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
await wrapper.vm.save()
|
|
const payload = save.mock.calls[0][0]
|
|
expect(payload.dimensions[0].select_item[0].key).toBe('yes')
|
|
expect(payload.dimensions[1].config.min).toBe(0)
|
|
expect(payload.dimensions[1].edit_input).toBe('number')
|
|
expect(payload.dimensions.every((d) => !d.need_fill && d.rule === '')).toBe(
|
|
true
|
|
)
|
|
expect(wrapper.emitted().saved).toBeTruthy()
|
|
wrapper.destroy()
|
|
})
|
|
it('saves the selected personal information field for automatic filling', async() => {
|
|
const wrapper = shallowMount(Editor, {
|
|
localVue,
|
|
propsData: {
|
|
question: {
|
|
name: '姓名',
|
|
field: 'student_name',
|
|
edit_input: 'text',
|
|
config: { user_field: 'name' }
|
|
}
|
|
}
|
|
})
|
|
expect(wrapper.vm.draft.config.user_field).toBe('name')
|
|
wrapper.vm.draft.config.user_field = 'company_name'
|
|
await wrapper.vm.save()
|
|
expect(save.mock.calls[0][0].config.user_field).toBe('company_name')
|
|
expect(wrapper.vm.userFields).not.toHaveProperty('password')
|
|
wrapper.destroy()
|
|
})
|
|
it('previews conditional questions and per-option notes with a selection limit', async() => {
|
|
questionIndex.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: 1,
|
|
field: 'insurance',
|
|
name: '保险',
|
|
edit_input: 'radio',
|
|
config: {},
|
|
select_item: [{ key: 'yes', value: '需要' }]
|
|
},
|
|
{
|
|
id: 2,
|
|
field: 'idcard',
|
|
name: '身份证',
|
|
edit_input: 'text',
|
|
config: { visible_when: { field: 'insurance', value: 'yes' }}
|
|
}
|
|
]
|
|
})
|
|
const state = { questions: (await questionIndex()).data, answers: {}}
|
|
expect(Fill.computed.shownQuestions.call(state)).toHaveLength(1)
|
|
state.answers.insurance = 'yes'
|
|
expect(Fill.computed.shownQuestions.call(state)).toHaveLength(2)
|
|
state.answers.insurance = 'no'
|
|
expect(Fill.computed.shownQuestions.call(state)).toHaveLength(1)
|
|
const warning = jest.fn()
|
|
const choice = mount(Question, {
|
|
localVue,
|
|
mocks: { $message: { warning }},
|
|
propsData: {
|
|
question: {
|
|
name: '会场',
|
|
edit_input: 'checkbox',
|
|
config: {
|
|
max_choices: 2,
|
|
option_notes: { tax: { placeholder: '咨询问题' }}
|
|
},
|
|
select_item: [
|
|
{ key: 'tax', value: '财税' },
|
|
{ key: 'legal', value: '法律' },
|
|
{ key: 'ip', value: '知识产权' }
|
|
]
|
|
}
|
|
}
|
|
})
|
|
choice.vm.setPreviewValue(['tax'])
|
|
await choice.vm.$nextTick()
|
|
expect(choice.find('textarea[placeholder="咨询问题"]').exists()).toBe(true)
|
|
choice.vm.setPreviewValue(['tax', 'legal', 'ip'])
|
|
expect(warning).toHaveBeenCalled()
|
|
expect(choice.vm.previewChecks).toEqual(['tax'])
|
|
choice.destroy()
|
|
})
|
|
it('edits conditions using only earlier choice questions and clears stale options', async() => {
|
|
const parent = {
|
|
id: 1,
|
|
field: 'insurance',
|
|
name: '保险',
|
|
edit_input: 'radio',
|
|
select_item: [{ key: 'yes', value: '需要' }]
|
|
}
|
|
const child = {
|
|
id: 2,
|
|
field: 'idcard',
|
|
name: '身份证',
|
|
edit_input: 'text',
|
|
config: { visible_when: { field: 'insurance', value: 'yes' }}
|
|
}
|
|
const warning = jest.fn()
|
|
const wrapper = shallowMount(Editor, {
|
|
localVue,
|
|
mocks: { $message: { warning, success: jest.fn() }},
|
|
propsData: {
|
|
question: child,
|
|
questions: [parent, child, { ...parent, id: 3, field: 'later' }]
|
|
}
|
|
})
|
|
expect(wrapper.vm.conditionQuestions.map((q) => q.field)).toEqual([
|
|
'insurance'
|
|
])
|
|
expect(wrapper.vm.draft.config.visible_when.value).toBe('yes')
|
|
await wrapper.vm.save()
|
|
expect(save.mock.calls[0][0].config.visible_when).toEqual({
|
|
field: 'insurance',
|
|
value: 'yes'
|
|
})
|
|
save.mockClear()
|
|
wrapper.vm.changeConditionQuestion('insurance')
|
|
await wrapper.vm.save()
|
|
expect(warning).toHaveBeenCalled()
|
|
expect(save).not.toHaveBeenCalled()
|
|
wrapper.vm.toggleCondition(false)
|
|
await wrapper.vm.save()
|
|
expect(save.mock.calls[0][0].config.visible_when).toBeUndefined()
|
|
wrapper.destroy()
|
|
})
|
|
it('hides dependent chains even when hidden parents retain an old answer', () => {
|
|
const state = {
|
|
questions: [
|
|
{ field: 'first', config: {}},
|
|
{
|
|
field: 'second',
|
|
config: { visible_when: { field: 'first', value: 'yes' }}
|
|
},
|
|
{
|
|
field: 'third',
|
|
config: { visible_when: { field: 'second', value: 'yes' }}
|
|
}
|
|
],
|
|
answers: { first: 'no', second: 'yes' }
|
|
}
|
|
expect(Fill.computed.shownQuestions.call(state)).toHaveLength(1)
|
|
state.answers.first = 'yes'
|
|
expect(Fill.computed.shownQuestions.call(state)).toHaveLength(3)
|
|
})
|
|
it('keeps a cleared numeric maximum unlimited instead of defaulting to zero', () => {
|
|
const field = {
|
|
edit_input: 'number',
|
|
config: { integer: true, min: 0, max: undefined }
|
|
}
|
|
const wrapper = mount(
|
|
{
|
|
render(h) {
|
|
return h('el-form', [h(FieldSettings, { props: { field }})])
|
|
}
|
|
},
|
|
{ localVue }
|
|
)
|
|
expect(field.config.max).toBeUndefined()
|
|
expect(wrapper.findAll('input[type="number"]').at(1).element.value).toBe(
|
|
''
|
|
)
|
|
wrapper.destroy()
|
|
})
|
|
it('renders zero as an answer and chapter headings without inputs', () => {
|
|
const wrapper = mount(Question, {
|
|
localVue,
|
|
propsData: {
|
|
answered: true,
|
|
question: { name: '数量', edit_input: 'number', display_value: '0' }
|
|
}
|
|
})
|
|
expect(wrapper.find('.answer').text()).toBe('0')
|
|
wrapper.destroy()
|
|
const section = mount(Question, {
|
|
localVue,
|
|
propsData: {
|
|
question: {
|
|
name: '课程评价',
|
|
edit_input: 'section',
|
|
help: '请选择评分'
|
|
}
|
|
}
|
|
})
|
|
expect(section.findAll('input').length).toBe(0)
|
|
expect(section.text()).toContain('请选择评分')
|
|
section.destroy()
|
|
})
|
|
it('shows score labels once and reveals selected supplementary input', async() => {
|
|
const wrapper = mount(Question, {
|
|
localVue,
|
|
propsData: {
|
|
question: {
|
|
name: '评分',
|
|
edit_input: 'radio',
|
|
config: { is_score: true, other_key: 'custom' },
|
|
select_item: [{ key: 'custom', value: '10分', score: 10 }]
|
|
}
|
|
}
|
|
})
|
|
expect(wrapper.text().match(/10分/g)).toHaveLength(1)
|
|
wrapper.setData({ previewValue: 'custom' })
|
|
await wrapper.vm.$nextTick()
|
|
expect(
|
|
wrapper.find('textarea[placeholder="请填写补充说明"]').exists()
|
|
).toBe(true)
|
|
wrapper.destroy()
|
|
})
|
|
it('shows all reply options with selected radio and checkbox values read-only', () => {
|
|
for (const type of ['radio', 'checkbox']) {
|
|
const wrapper = mount(Question, {
|
|
localVue,
|
|
propsData: {
|
|
answered: true,
|
|
question: {
|
|
name: '选择',
|
|
edit_input: type,
|
|
selected_keys: ['0'],
|
|
display_value: '零分',
|
|
select_item: [
|
|
{ key: 0, value: '零分' },
|
|
{ key: 'other', value: '其他' }
|
|
]
|
|
}
|
|
}
|
|
})
|
|
expect(wrapper.findAll('input')).toHaveLength(2)
|
|
expect(wrapper.findAll('input').at(0).element.checked).toBe(true)
|
|
expect(wrapper.findAll('input').at(1).element.checked).toBe(false)
|
|
expect(
|
|
wrapper
|
|
.findAll('input')
|
|
.wrappers.every((input) => input.element.disabled)
|
|
).toBe(true)
|
|
expect(wrapper.text()).toContain('学员选择:零分')
|
|
expect(wrapper.text()).toContain('其他')
|
|
wrapper.destroy()
|
|
}
|
|
const combo = mount(Question, {
|
|
localVue,
|
|
propsData: {
|
|
answered: true,
|
|
question: {
|
|
name: '组合',
|
|
edit_input: 'multi_dimension',
|
|
dimensions: [
|
|
{
|
|
field: 'intent',
|
|
name: '意向',
|
|
selected_keys: ['no'],
|
|
display_value: '否',
|
|
select_item: [
|
|
{ key: 'yes', value: '是' },
|
|
{ key: 'no', value: '否' }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
expect(combo.findAll('input')).toHaveLength(2)
|
|
expect(combo.findAll('input').at(1).element.checked).toBe(true)
|
|
combo.destroy()
|
|
})
|
|
it('uses wrapping scores and vertical ordinary choices for preview fields', () => {
|
|
const score = mount(PreviewField, {
|
|
localVue,
|
|
propsData: {
|
|
field: {
|
|
edit_input: 'radio',
|
|
config: { is_score: true },
|
|
select_item: [{ key: '5', value: '5分' }]
|
|
}
|
|
}
|
|
})
|
|
expect(score.find('.scores').exists()).toBe(true)
|
|
score.destroy()
|
|
const ordinary = mount(PreviewField, {
|
|
localVue,
|
|
propsData: {
|
|
field: {
|
|
edit_input: 'radio',
|
|
config: {},
|
|
select_item: [{ key: 'yes', value: '是' }]
|
|
}
|
|
}
|
|
})
|
|
expect(ordinary.find('.scores').exists()).toBe(false)
|
|
ordinary.destroy()
|
|
const number = mount(PreviewField, {
|
|
localVue,
|
|
propsData: {
|
|
field: { edit_input: 'number', config: { integer: true, min: 0 }}
|
|
}
|
|
})
|
|
expect(number.find('input').attributes('placeholder')).toBe('请输入数量')
|
|
expect(number.text()).not.toContain('最小')
|
|
number.destroy()
|
|
})
|
|
it('renders rich descriptions safely and preserves plain text', () => {
|
|
const rich = mount(Description, {
|
|
propsData: {
|
|
value:
|
|
'<p style="text-indent:2em"><strong>说明</strong><img src="/a.png" onerror="alert(1)"><script>alert(1)</script></p>'
|
|
}
|
|
})
|
|
expect(rich.find('strong').text()).toBe('说明')
|
|
expect(rich.find('p').element.style.textIndent).toBe('2em')
|
|
expect(rich.find('script').exists()).toBe(false)
|
|
expect(rich.find('img').attributes('onerror')).toBeUndefined()
|
|
rich.destroy()
|
|
const plain = mount(Description, {
|
|
propsData: { value: '第一行\n第二行' }
|
|
})
|
|
expect(plain.text()).toContain('第一行\n第二行')
|
|
plain.destroy()
|
|
})
|
|
it('loads real statistics and the server reply snapshot', async() => {
|
|
statistics.mockResolvedValue({
|
|
responses: 2,
|
|
questions_count: 1,
|
|
average: 4,
|
|
questions: [
|
|
{
|
|
id: 1,
|
|
name: '满意度',
|
|
edit_input: 'radio',
|
|
stats: {
|
|
valid_count: 2,
|
|
average: 4,
|
|
other_count: 0,
|
|
options: [{ label: '满意', count: 2, percentage: 100 }]
|
|
}
|
|
}
|
|
]
|
|
})
|
|
show.mockResolvedValue({
|
|
id: 7,
|
|
snapshot: {
|
|
title: '提交时的标题',
|
|
desc: '<p><strong>历史问卷说明</strong></p>'
|
|
},
|
|
user: { name: '学员' },
|
|
questions: [
|
|
{ id: 1, name: '历史题目', edit_input: 'number', display_value: '0' }
|
|
]
|
|
})
|
|
const wrapper = mount(Results, {
|
|
localVue,
|
|
propsData: {
|
|
visible: false,
|
|
surveyData: { id: 10, title: '现在的标题' }
|
|
}
|
|
})
|
|
wrapper.setProps({ visible: true })
|
|
await flush()
|
|
expect(statistics).toHaveBeenCalledWith({
|
|
course_content_evaluation_id: 10
|
|
})
|
|
expect(wrapper.text()).toContain('有效作答 2 人')
|
|
await wrapper.vm.openReply({ id: 7 })
|
|
await flush()
|
|
expect(wrapper.vm.reply.snapshot.title).toBe('提交时的标题')
|
|
expect(
|
|
document.body.querySelector('.reply .description strong').textContent
|
|
).toBe('历史问卷说明')
|
|
expect(wrapper.text()).not.toContain('<strong>')
|
|
expect(wrapper.vm.reply.questions[0].display_value).toBe('0')
|
|
wrapper.destroy()
|
|
})
|
|
it('shows file names and downloads with original names', async() => {
|
|
const file = { id: 81, original_name: '材料.pdf', url: '/storage/files/a.pdf', size: 2048 }
|
|
const answer = mount(Question, { localVue, propsData: { answered: true, question: { edit_input: 'file', name: '附件', value: [file] } } })
|
|
expect(answer.text()).toContain('材料.pdf')
|
|
expect(answer.text()).toContain('下载')
|
|
const blob = new Blob(['example'])
|
|
const get = jest.spyOn(axios, 'get').mockResolvedValue({ headers: { 'content-type': 'application/pdf' }, data: blob })
|
|
const oldFetch = global.fetch
|
|
global.fetch = jest.fn().mockResolvedValue({ ok: true, blob: () => Promise.resolve(blob) })
|
|
await answer.find(Attachments).vm.download(file)
|
|
expect(saveAs).toHaveBeenCalledWith(blob, '材料.pdf')
|
|
expect(get).toHaveBeenCalledWith('/api/admin/course-content-evaluation-form/show', expect.objectContaining({ params: expect.objectContaining({ download_file_id: 81 }) }))
|
|
get.mockRestore()
|
|
global.fetch = oldFetch
|
|
answer.destroy()
|
|
})
|
|
it('edits allowed upload formats and shows them in preview', async() => {
|
|
const field = { edit_input: 'file', config: { max_files: 2, allowed_extensions: ['pdf'] } }
|
|
const wrapper = shallowMount(FieldSettings, { localVue, propsData: { field } })
|
|
wrapper.find('el-select-stub').vm.$emit('change', ['pdf', 'docx'])
|
|
await wrapper.vm.$nextTick()
|
|
expect(field.config.allowed_extensions).toEqual(['pdf', 'docx'])
|
|
const preview = mount(PreviewField, { localVue, propsData: { field } })
|
|
expect(preview.text()).toContain('PDF、DOCX')
|
|
wrapper.destroy()
|
|
preview.destroy()
|
|
})
|
|
it('configures per-option notes, placeholders and selection limits without a new type', async() => {
|
|
const field = { edit_input: 'checkbox', select_item: [{ key: 'tax', value: '财税' }, { key: 'legal', value: '法律' }], config: { max_choices: 1, other_key: 'tax', other_placeholder: '补充说明提示' } }
|
|
const settings = shallowMount(FieldSettings, { localVue, propsData: { field } })
|
|
settings.vm.toggleNote('tax', true)
|
|
expect(field.config.other_key).toBe('')
|
|
field.config.option_notes.tax.placeholder = '可初步填写您想咨询的问题'
|
|
field.config.option_notes.tax.required = true
|
|
const editor = shallowMount(Editor, { localVue, propsData: { question: { ...field, name: '会场', field: 'venues' } } })
|
|
await editor.vm.save()
|
|
expect(save.mock.calls[0][0].config).toMatchObject({ max_choices: 1, other_placeholder: '补充说明提示', option_notes: { tax: { placeholder: '可初步填写您想咨询的问题', required: true } } })
|
|
settings.vm.removeOption(0)
|
|
expect(field.config.option_notes.tax).toBeUndefined()
|
|
settings.destroy()
|
|
editor.destroy()
|
|
})
|
|
})
|