165 lines
7.2 KiB
JavaScript
165 lines
7.2 KiB
JavaScript
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const vm = require('node:vm')
|
|
const ts = require('typescript')
|
|
const vue = require('vue')
|
|
const { parse, compileScript, compileTemplate } = require('vue/compiler-sfc')
|
|
|
|
const filename = path.join(__dirname, '../src/views/app/setting/index.vue')
|
|
const { descriptor } = parse(fs.readFileSync(filename, 'utf8'))
|
|
const transpile = (source) => ts.transpileModule(source, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
|
}).outputText
|
|
function evaluate(source, imports, globals = {}) {
|
|
const module = { exports: {} }
|
|
vm.runInNewContext(transpile(source), {
|
|
exports: module.exports,
|
|
require: (name) => {
|
|
if (!(name in imports)) throw new Error(`Unexpected import: ${name}`)
|
|
return imports[name]
|
|
},
|
|
...globals
|
|
})
|
|
return module.exports
|
|
}
|
|
const api = evaluate(fs.readFileSync(path.join(__dirname, '../src/api/app/setting/index.ts'), 'utf8'), {
|
|
'@/config/axios': { default: {} }
|
|
})
|
|
function harness(overrides = {}, editable = true) {
|
|
const calls = []
|
|
const backend = {
|
|
...api,
|
|
getSetting: async () => null,
|
|
getRooms: async () => [],
|
|
saveSetting: async (data) => { calls.push(['setting', data]); return true },
|
|
getBookingPage: async () => ({ list: [], total: 0 }),
|
|
getClosurePage: async () => ({ list: [], total: 0 }),
|
|
updateBookingStatus: async (data) => { calls.push(['booking', data]); return true },
|
|
updateClosureStatus: async (data) => { calls.push(['closure', data]); return true },
|
|
...overrides
|
|
}
|
|
const component = evaluate(compileScript(descriptor, { id: 'app-setting' }).content, {
|
|
vue: { ...vue, onMounted: () => {} },
|
|
'vue-router': { onBeforeRouteLeave: () => {} },
|
|
'element-plus': { ElMessageBox: { prompt: async () => ({ value: '客人要求取消' }) } },
|
|
'@/components/UploadFile': { UploadImg: {} },
|
|
'@/utils/permission': { checkPermi: () => editable },
|
|
'@/utils/formatTime': { formatDate: (value) => value },
|
|
'@/api/app/setting': backend
|
|
}, {
|
|
useMessage: () => ({ success: () => {}, warning: () => {}, confirm: async () => true })
|
|
}).default
|
|
return { page: component.setup({}, { expose: () => {} }), calls, backend }
|
|
}
|
|
const deferred = () => { let resolve; const promise = new Promise((done) => { resolve = done }); return { promise, resolve } }
|
|
|
|
test('settings template compiles with all five management sections', () => {
|
|
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename, id: 'app-setting' }).errors, [])
|
|
for (const section of ['运行模式', '门店信息', '包间管理', '预约管理', '注销申请']) assert.ok(descriptor.template.content.includes(section))
|
|
})
|
|
|
|
test('price inputs convert decimal currency exactly and reject unsupported values', () => {
|
|
assert.equal(api.yuanToFen('0.29'), 29)
|
|
assert.equal(api.yuanToFen('1.01'), 101)
|
|
assert.equal(api.yuanToFen('21474836.47'), 2147483647)
|
|
assert.equal(api.yuanToFen('21474836.48'), null)
|
|
for (const invalid of ['-1', '1.001', '1e4', '', '1,000', 'Infinity']) assert.equal(api.yuanToFen(invalid), null)
|
|
assert.equal(api.fenToYuan(1), '0.01')
|
|
assert.equal(api.fenToYuan('900719925474099301'), '9007199254740993.01')
|
|
})
|
|
|
|
test('tea activation requires configured store, valid non-overlapping slots and an enabled room', async () => {
|
|
const { page, calls } = harness()
|
|
await page.loadSettings(); await page.loadRooms()
|
|
page.settings.iosMode = 'tea'
|
|
await page.submitSettings()
|
|
assert.equal(calls.length, 0)
|
|
assert.ok(page.settingErrors.value.activation)
|
|
Object.assign(page.settings, {
|
|
storeName: '茶舍', storeCover: '/cover.jpg', storeAddress: '门店地址', storePhone: '12345678',
|
|
openTime: '09:00', closeTime: '18:00', slots: [{ startTime: '10:00', endTime: '12:00' }, { startTime: '11:30', endTime: '14:00' }]
|
|
})
|
|
page.rooms.value = [{ id: 1, enabled: true }]
|
|
await page.submitSettings()
|
|
assert.equal(calls.length, 0)
|
|
assert.match(page.settingErrors.value.slots, /重叠/)
|
|
page.settings.slots[1].startTime = '12:00'
|
|
await page.submitSettings()
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0][1].iosMode, 'tea')
|
|
assert.equal(calls[0][1].androidMode, 'chat')
|
|
assert.equal(page.settingsDirty.value, false)
|
|
})
|
|
|
|
test('save failure retains user draft and unsaved state', async () => {
|
|
const { page } = harness({ saveSetting: async () => { throw new Error('network') } })
|
|
await page.loadSettings()
|
|
page.settings.storeName = '尚未保存的茶舍'
|
|
await page.submitSettings()
|
|
assert.equal(page.settings.storeName, '尚未保存的茶舍')
|
|
assert.equal(page.settingsDirty.value, true)
|
|
assert.equal(page.settingsSaving.value, false)
|
|
})
|
|
|
|
test('chat mode settings retain valid default hours and cannot save blank hours', async () => {
|
|
const { page, calls } = harness()
|
|
await page.loadSettings()
|
|
assert.equal(page.settings.openTime, '10:00')
|
|
assert.equal(page.settings.closeTime, '22:00')
|
|
page.settings.openTime = ''
|
|
page.settings.closeTime = ''
|
|
await page.submitSettings()
|
|
assert.equal(calls.length, 0)
|
|
assert.ok(page.settingErrors.value.businessHours)
|
|
})
|
|
|
|
test('late booking responses cannot overwrite the current filter result', async () => {
|
|
const first = deferred(); const second = deferred(); let count = 0
|
|
const { page } = harness({ getBookingPage: () => (++count === 1 ? first.promise : second.promise) })
|
|
const a = page.loadBookings(); const b = page.loadBookings()
|
|
second.resolve({ list: [{ id: 2 }], total: 1 }); await b
|
|
first.resolve({ list: [{ id: 1 }], total: 3 }); await a
|
|
assert.equal(page.bookings.value[0].id, 2)
|
|
assert.equal(page.bookingTotal.value, 1)
|
|
})
|
|
|
|
test('booking actions use server capabilities and do not submit unavailable actions', async () => {
|
|
const { page, calls } = harness()
|
|
const booking = { id: 1, canConfirm: false, canReject: true, canComplete: false, canAdminCancel: false }
|
|
await page.changeBooking(booking, 'confirmed')
|
|
assert.equal(calls.length, 0)
|
|
await page.changeBooking(booking, 'rejected')
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0][1].status, 'rejected')
|
|
assert.equal(calls[0][1].reason, '客人要求取消')
|
|
})
|
|
|
|
test('account closure requires processing remarks and only records an outcome', async () => {
|
|
const { page, calls } = harness()
|
|
page.openClosure({ id: 7, status: 'pending' })
|
|
await page.submitClosure()
|
|
assert.equal(calls.length, 0)
|
|
assert.ok(page.closureRemarkError.value)
|
|
page.closureRemark.value = '已核实申请并完成必要的人工处理'
|
|
page.closureNextStatus.value = 'resolved'
|
|
await page.submitClosure()
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0][0], 'closure')
|
|
assert.equal(calls[0][1].status, 'resolved')
|
|
assert.equal(page.closureDialog.value, false)
|
|
})
|
|
|
|
test('read-only operators cannot save settings or process requests', async () => {
|
|
const { page, calls } = harness({}, false)
|
|
await page.loadSettings()
|
|
page.settings.storeName = '无写权限'
|
|
await page.submitSettings()
|
|
page.openClosure({ id: 7, status: 'pending' })
|
|
page.closureRemark.value = '测试'
|
|
await page.submitClosure()
|
|
await page.changeBooking({ id: 1, canConfirm: true }, 'confirmed')
|
|
assert.equal(calls.length, 0)
|
|
})
|