738 lines
23 KiB
JavaScript
738 lines
23 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
import { compileScript, parse } from '@vue/compiler-sfc'
|
|
|
|
const source = (path) => readFileSync(path, 'utf8')
|
|
function evaluate(text, imports = {}, globals = {}) {
|
|
const compiled = ts.transpileModule(text, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
|
}).outputText
|
|
const module = { exports: {} }
|
|
new Function('require', 'module', 'exports', ...Object.keys(globals), compiled)(
|
|
(name) => {
|
|
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
|
return imports[name]
|
|
},
|
|
module,
|
|
module.exports,
|
|
...Object.values(globals)
|
|
)
|
|
return module.exports
|
|
}
|
|
const format = evaluate(source('src/utils/business/format.ts'))
|
|
const gateway = evaluate(source('src/utils/business/gateway.ts'), { './format': format })
|
|
const constants = evaluate(source('src/config/constants/pollingConfig.ts'))
|
|
const helpers = evaluate(source('src/utils/business/pollingConfig.ts'), {
|
|
'@/config/constants/pollingConfig': constants,
|
|
'./gateway': gateway
|
|
})
|
|
const perms = constants.POLLING_CONFIG_PERMISSIONS
|
|
const ok = (data) => ({ code: 0, data, msg: 'ok' })
|
|
const page = (items) => ok({ items, page: 1, page_size: 20, total: items.length })
|
|
const draft = () => ({ ...helpers.newPollingDraft(), config_name: '测试草稿' })
|
|
const task = (id = 'task-1', status = 'running') => ({
|
|
task_id: id,
|
|
status,
|
|
total: 100,
|
|
processed: 20,
|
|
advanced: 10,
|
|
carrier_id: '8',
|
|
error: '',
|
|
created_at: '2026-10-26T10:00:00+08:00',
|
|
started_at: '',
|
|
finished_at: ''
|
|
})
|
|
const preview = {
|
|
card: { iccid: 'CARD', asset_status: 2, package_state: 'active' },
|
|
matched: [],
|
|
intervals: [{ task_type: 'polling:carddata', mode: 'disabled', interval: 0 }]
|
|
}
|
|
const preReset = {
|
|
carriers: [
|
|
{
|
|
carrier_id: 8,
|
|
carrier_name: '账户',
|
|
previous: {
|
|
active_package_cards: 0,
|
|
checked_in_window: 0,
|
|
not_checked_in_window: 0,
|
|
reset_at: '2026-10-26T00:00:00+08:00',
|
|
last_offset_buckets: { le_5: 0, le_15: 0, le_30: 0, le_60: 0, gt_60: 0 }
|
|
},
|
|
upcoming: null
|
|
}
|
|
]
|
|
}
|
|
const tiers = {
|
|
total_cards: 0,
|
|
by_config: [{ config_id: 0, config_name: '', cards: 0 }],
|
|
by_package_state: { active: 0, unknown: 0 },
|
|
computed_at: '2026-10-26T00:00:00Z',
|
|
unmatched_in_stock: 0,
|
|
unmatched_sold_no_package: 0
|
|
}
|
|
function deferred() {
|
|
let resolve
|
|
let reject
|
|
const promise = new Promise((done, fail) => {
|
|
resolve = done
|
|
reject = fail
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
async function flush() {
|
|
await new Promise((done) => setImmediate(done))
|
|
await vue.nextTick()
|
|
}
|
|
|
|
test('all seven new API methods use exact URLs, methods, query and body contracts', async () => {
|
|
const calls = []
|
|
const response = ok(preview)
|
|
const http = Object.fromEntries(
|
|
['get', 'post', 'put', 'del'].map((method) => [
|
|
method,
|
|
(args) => {
|
|
calls.push({ method, ...args })
|
|
return Promise.resolve(response)
|
|
}
|
|
])
|
|
)
|
|
const base = evaluate(source('src/api/BaseService.ts'), { '@/utils/http': { default: http } })
|
|
const { PollingConfigService: service } = evaluate(source('src/api/modules/pollingConfig.ts'), {
|
|
'../BaseService': base
|
|
})
|
|
const body = { iccid: 'CARD/1', overrides: [draft()] }
|
|
const specs = [
|
|
[
|
|
'previewMatch',
|
|
['CARD/1'],
|
|
{
|
|
method: 'get',
|
|
url: '/api/admin/polling-configs/match-preview',
|
|
params: { iccid: 'CARD/1' }
|
|
}
|
|
],
|
|
[
|
|
'previewDraft',
|
|
[body],
|
|
{
|
|
method: 'post',
|
|
url: '/api/admin/polling-configs/match-preview',
|
|
params: { iccid: body.iccid },
|
|
data: body
|
|
}
|
|
],
|
|
[
|
|
'updatePriorities',
|
|
[[12, 3, 8]],
|
|
{
|
|
method: 'put',
|
|
url: '/api/admin/polling-configs/priorities',
|
|
data: { ordered_ids: [12, 3, 8] }
|
|
}
|
|
],
|
|
[
|
|
'recalculateDue',
|
|
[null],
|
|
{
|
|
method: 'post',
|
|
url: '/api/admin/polling-configs/recalculate-due',
|
|
data: { carrier_id: null }
|
|
}
|
|
],
|
|
[
|
|
'getRecalculateTask',
|
|
['task/a b'],
|
|
{
|
|
method: 'get',
|
|
url: '/api/admin/polling-configs/recalculate-due/task%2Fa%20b',
|
|
params: undefined
|
|
}
|
|
],
|
|
[
|
|
'getPreResetStats',
|
|
[],
|
|
{ method: 'get', url: '/api/admin/polling-stats/pre-reset', params: undefined }
|
|
],
|
|
[
|
|
'getTierStats',
|
|
[],
|
|
{ method: 'get', url: '/api/admin/polling-stats/tiers', params: undefined }
|
|
]
|
|
]
|
|
for (const [name, args, expected] of specs) {
|
|
assert.equal(await service[name](...args), response)
|
|
assert.deepEqual(calls.at(-1), expected)
|
|
}
|
|
})
|
|
|
|
test('drafts preserve null inheritance, zero disable, numeric intervals and body whitelist', () => {
|
|
const item = {
|
|
...draft(),
|
|
carrier_id: 0,
|
|
realname_check_interval: null,
|
|
carddata_check_interval: 0,
|
|
package_check_interval: 60,
|
|
status: 0,
|
|
key: 'ui',
|
|
created_at: 'unexpected'
|
|
}
|
|
const [body] = helpers.pollingDraftBody([item])
|
|
assert.equal(body.carrier_id, 0)
|
|
assert.equal(body.realname_check_interval, null)
|
|
assert.equal(body.carddata_check_interval, 0)
|
|
assert.equal(body.package_check_interval, 60)
|
|
for (const key of ['status', 'key', 'created_at']) assert.equal(key in body, false)
|
|
assert.equal(item.status, 0)
|
|
assert.equal(helpers.pollingDraftBody([draft(), draft()]).length, 2)
|
|
assert.throws(() =>
|
|
helpers.pollingDraftBody([
|
|
{ ...draft(), id: 1 },
|
|
{ ...draft(), id: 1 }
|
|
])
|
|
)
|
|
})
|
|
|
|
test('draft validation enforces documented interval minima, priority and paired windows', () => {
|
|
for (const patch of [
|
|
{ config_name: ' ' },
|
|
{ config_name: 'a'.repeat(101) },
|
|
{ description: 'x'.repeat(501) },
|
|
{ priority: 0 },
|
|
{ priority: 1001 },
|
|
{ priority: 1.2 },
|
|
{ asset_status: 5 },
|
|
{ carrier_id: -1 },
|
|
{ realname_check_interval: 29 },
|
|
{ carddata_check_interval: 59 },
|
|
{ package_check_interval: -1 },
|
|
{ card_status_check_interval: 0.1 },
|
|
{ pre_reset_window: 60 },
|
|
{ pre_reset_window: 59, pre_reset_carddata_interval: 60 }
|
|
])
|
|
assert.throws(() => helpers.pollingDraftBody([{ ...draft(), ...patch }]))
|
|
assert.doesNotThrow(() =>
|
|
helpers.pollingDraftBody([
|
|
{
|
|
...draft(),
|
|
protect_check_interval: 30,
|
|
pre_reset_window: 60,
|
|
pre_reset_carddata_interval: 60
|
|
}
|
|
])
|
|
)
|
|
assert.equal(helpers.pollingIccid(' CARD '), 'CARD')
|
|
assert.throws(() => helpers.pollingIccid(''))
|
|
assert.throws(() => helpers.pollingIccid('x'.repeat(31)))
|
|
for (const ids of [[], [1, 1], [-1], [0], [1.5]])
|
|
assert.throws(() => helpers.validatePollingOrder(ids))
|
|
assert.doesNotThrow(() => helpers.validatePollingOrder([3, 2, 1]))
|
|
})
|
|
|
|
function mount(
|
|
t,
|
|
path,
|
|
{ superAdmin = false, permissions = [], overrides = {}, props = {}, globals = {} } = {}
|
|
) {
|
|
const calls = []
|
|
const events = []
|
|
const user = vue.reactive({ isSuperAdmin: superAdmin, permissions })
|
|
const config = {
|
|
id: 1,
|
|
config_name: '默认配置',
|
|
priority: 1,
|
|
status: 1,
|
|
carrier_id: 8,
|
|
realname_check_interval: null
|
|
}
|
|
const defaults = {
|
|
getPollingConfigs: () => page([config]),
|
|
getPollingConfigById: () => ok(config),
|
|
previewMatch: () => ok(preview),
|
|
previewDraft: () => ok(preview),
|
|
updatePriorities: () => undefined,
|
|
recalculateDue: () => ok(task()),
|
|
getRecalculateTask: (id) => ok(task(id)),
|
|
getPreResetStats: () => ok(preReset),
|
|
getTierStats: () => ok(tiers)
|
|
}
|
|
const service = Object.fromEntries(
|
|
Object.entries(defaults).map(([name, handler]) => [
|
|
name,
|
|
async (...args) => {
|
|
calls.push({ name, args })
|
|
return (overrides[name] || handler)(...args)
|
|
}
|
|
])
|
|
)
|
|
const hasAuth = (value) => user.isSuperAdmin || user.permissions.includes(value)
|
|
const imports = {
|
|
vue,
|
|
'element-plus': {
|
|
ElMessageBox: { confirm: overrides.confirm || (async () => {}) },
|
|
ElMessage: { success() {} }
|
|
},
|
|
'@vueuse/core': { useWindowSize: () => ({ width: vue.ref(1200) }) },
|
|
'@/api/modules/pollingConfig': { PollingConfigService: service },
|
|
'@/store/modules/user': { useUserStore: () => user },
|
|
'@/composables/useAuth': { useAuth: () => ({ hasAuth }) },
|
|
'@/config/constants/pollingConfig': constants,
|
|
'@/utils/business/pollingConfig': helpers,
|
|
'@/utils/business/gateway': gateway,
|
|
'@/utils/business/format': format,
|
|
'@/composables/useGateway': {
|
|
useGatewayCarriers: () => ({
|
|
options: vue.ref([]),
|
|
loading: vue.ref(false),
|
|
error: vue.ref(''),
|
|
search() {},
|
|
select() {},
|
|
label: String
|
|
}),
|
|
useGatewayColumns: (definitions) => ({
|
|
columns: vue.computed(() => definitions),
|
|
columnChecks: vue.ref(definitions)
|
|
})
|
|
},
|
|
'./PollingDraftFields.vue': { default: {} }
|
|
}
|
|
imports['@/composables/usePollingConfigExtensions'] = evaluate(
|
|
source('src/composables/usePollingConfigExtensions.ts'),
|
|
imports,
|
|
globals
|
|
)
|
|
let component
|
|
if (path === 'progress')
|
|
component = {
|
|
setup: () =>
|
|
imports['@/composables/usePollingConfigExtensions'].usePollingRecalculateProgress(() =>
|
|
hasAuth(perms.progress)
|
|
)
|
|
}
|
|
else {
|
|
const { descriptor } = parse(source(path))
|
|
component = evaluate(
|
|
compileScript(descriptor, { id: 'polling-config-test' }).content,
|
|
imports,
|
|
globals
|
|
).default
|
|
}
|
|
let state
|
|
const renderer = vue.createRenderer({
|
|
createComment: () => ({}),
|
|
insert() {},
|
|
remove() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null
|
|
})
|
|
const reactiveProps = vue.reactive(props)
|
|
const app = renderer.createApp({
|
|
setup() {
|
|
state = component.setup(reactiveProps, { expose() {}, emit: (...args) => events.push(args) })
|
|
return () => null
|
|
}
|
|
})
|
|
app.mount({})
|
|
t.after(() => app.unmount())
|
|
return { state, calls, events, user, unmount: () => app.unmount() }
|
|
}
|
|
const previewPath = 'src/views/polling-management/config/components/PollingPreviewDialog.vue'
|
|
const priorityPath = 'src/views/polling-management/config/components/PollingPriorityDialog.vue'
|
|
const recalculatePath =
|
|
'src/views/polling-management/config/components/PollingRecalculateDialog.vue'
|
|
const statsPath = 'src/views/polling-management/config-stats/index.vue'
|
|
const writes = (calls) =>
|
|
calls.filter((call) =>
|
|
['updatePriorities', 'recalculateDue', 'createPollingConfig', 'updatePollingConfig'].includes(
|
|
call.name
|
|
)
|
|
)
|
|
|
|
test('preview permissions and ICCID validation block unauthorized or invalid requests', async (t) => {
|
|
const { state, calls, user } = mount(t, previewPath)
|
|
await flush()
|
|
state.iccid.value = 'CARD'
|
|
await state.runPreview()
|
|
assert.match(state.error.value, /无权/)
|
|
user.permissions.push(perms.preview)
|
|
state.iccid.value = 'x'.repeat(31)
|
|
await state.runPreview()
|
|
assert.match(state.error.value, /ICCID/)
|
|
assert.equal(
|
|
calls.some((call) => call.name === 'previewMatch'),
|
|
false
|
|
)
|
|
})
|
|
|
|
test('GET and multi-override POST previews never save configurations', async (t) => {
|
|
const { state, calls } = mount(t, previewPath, { permissions: [perms.preview] })
|
|
state.iccid.value = 'CARD'
|
|
await state.runPreview()
|
|
assert.equal(state.result.value.intervals[0].interval, 0)
|
|
state.useDraft.value = true
|
|
state.addDraft(draft())
|
|
state.addDraft({ ...draft(), id: 2, disabled: true })
|
|
await state.runPreview()
|
|
const body = calls.find((call) => call.name === 'previewDraft').args[0]
|
|
assert.equal(body.iccid, 'CARD')
|
|
assert.equal(body.overrides.length, 2)
|
|
assert.equal(body.overrides[1].disabled, true)
|
|
assert.equal(writes(calls).length, 0)
|
|
})
|
|
|
|
test('a stale preview cannot overwrite a changed card or unmounted dialog', async (t) => {
|
|
const pending = deferred()
|
|
const { state, unmount } = mount(t, previewPath, {
|
|
permissions: [perms.preview],
|
|
overrides: { previewMatch: () => pending.promise }
|
|
})
|
|
state.iccid.value = 'OLD'
|
|
const running = state.runPreview()
|
|
state.iccid.value = 'NEW'
|
|
pending.resolve(ok(preview))
|
|
await running
|
|
assert.equal(state.result.value, null)
|
|
unmount()
|
|
})
|
|
|
|
test('ordinary users cannot submit priority updates or start recalculation', async (t) => {
|
|
for (const [path, action] of [
|
|
[priorityPath, 'save'],
|
|
[recalculatePath, 'start']
|
|
]) {
|
|
const { state, calls } = mount(t, path, { permissions: [perms.progress] })
|
|
await flush()
|
|
await state[action]()
|
|
assert.equal(writes(calls).length, 0)
|
|
}
|
|
})
|
|
|
|
test('priority updates load all pages, preserve chosen order, confirm and accept empty success DTO', async (t) => {
|
|
const { state, calls, events } = mount(t, priorityPath, {
|
|
superAdmin: true,
|
|
overrides: {
|
|
getPollingConfigs: ({ page: current }) =>
|
|
ok({ items: [{ id: current, config_name: `配置${current}`, priority: current }], total: 2 })
|
|
}
|
|
})
|
|
await flush()
|
|
assert.equal(state.rows.value.length, 2)
|
|
state.move(1, -1)
|
|
await state.save()
|
|
assert.deepEqual(calls.find((call) => call.name === 'updatePriorities').args[0], [2, 1])
|
|
assert.ok(events.some((event) => event[0] === 'saved'))
|
|
})
|
|
|
|
test('priority cancellation or role revocation after confirmation sends no write', async (t) => {
|
|
for (const cancel of [true, false]) {
|
|
const confirmation = deferred()
|
|
const { state, calls, user } = mount(t, priorityPath, {
|
|
superAdmin: true,
|
|
overrides: { confirm: () => confirmation.promise }
|
|
})
|
|
await flush()
|
|
const saving = state.save()
|
|
if (cancel) confirmation.reject('cancel')
|
|
else {
|
|
user.isSuperAdmin = false
|
|
confirmation.resolve('ok')
|
|
}
|
|
await saving
|
|
assert.equal(writes(calls).length, 0)
|
|
}
|
|
})
|
|
|
|
test('recalculation uses numeric/null carrier IDs and only reports actual task status', async (t) => {
|
|
const { state, calls } = mount(t, recalculatePath, { superAdmin: true })
|
|
await state.start()
|
|
assert.equal(calls.find((call) => call.name === 'recalculateDue').args[0], null)
|
|
assert.equal(state.task.value.status, 'running')
|
|
state.allCarriers.value = false
|
|
state.carrierId.value = 8
|
|
await state.start()
|
|
assert.equal(calls.filter((call) => call.name === 'recalculateDue').at(-1).args[0], 8)
|
|
})
|
|
|
|
function fakeTimers() {
|
|
let key = 0
|
|
const timers = new Map()
|
|
return {
|
|
timers,
|
|
globals: {
|
|
setTimeout: (fn) => {
|
|
timers.set(++key, fn)
|
|
return key
|
|
},
|
|
clearTimeout: (id) => timers.delete(id)
|
|
},
|
|
async tick() {
|
|
const next = timers.entries().next().value
|
|
if (next) {
|
|
timers.delete(next[0])
|
|
next[1]()
|
|
await flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test('progress polling stops at terminal state and cleanup removes pending timers', async (t) => {
|
|
const clock = fakeTimers()
|
|
let count = 0
|
|
const { state, unmount } = mount(t, 'progress', {
|
|
permissions: [perms.progress],
|
|
globals: clock.globals,
|
|
overrides: { getRecalculateTask: (id) => ok(task(id, ++count === 1 ? 'running' : 'completed')) }
|
|
})
|
|
state.taskId.value = 'task-1'
|
|
state.autoRefresh.value = true
|
|
await state.query()
|
|
assert.equal(clock.timers.size, 1)
|
|
await clock.tick()
|
|
assert.equal(state.task.value.status, 'completed')
|
|
assert.equal(clock.timers.size, 0)
|
|
state.adopt(task())
|
|
assert.equal(clock.timers.size, 1)
|
|
unmount()
|
|
assert.equal(clock.timers.size, 0)
|
|
})
|
|
|
|
test('changing task ID, closing, errors or loss of permission discard old progress', async (t) => {
|
|
const clock = fakeTimers()
|
|
const pending = deferred()
|
|
const { state, user } = mount(t, 'progress', {
|
|
permissions: [perms.progress],
|
|
globals: clock.globals,
|
|
overrides: { getRecalculateTask: () => pending.promise }
|
|
})
|
|
state.taskId.value = 'OLD'
|
|
const request = state.query()
|
|
state.taskId.value = 'NEW'
|
|
pending.resolve(ok(task('OLD')))
|
|
await request
|
|
assert.equal(state.task.value, null)
|
|
state.adopt(task('NEW'))
|
|
state.autoRefresh.value = true
|
|
user.permissions = []
|
|
await flush()
|
|
assert.equal(clock.timers.size, 0)
|
|
assert.equal(state.task.value, null)
|
|
})
|
|
|
|
test('statistics retain null periods, zero counts, raw bucket values and unknown package states', async (t) => {
|
|
const { state } = mount(t, statsPath, {
|
|
permissions: [perms.statsPage, perms.preReset, perms.tiers]
|
|
})
|
|
await flush()
|
|
assert.equal(state.preRows.value.length, 2)
|
|
assert.equal(state.preValue(state.preRows.value[0], 'checked_in_window'), 0)
|
|
assert.equal(state.preValue(state.preRows.value[0], 'le_5'), 0)
|
|
assert.equal(state.preValue(state.preRows.value[1], 'reset_at'), '无周期数据')
|
|
assert.equal(state.preValue(state.preRows.value[1], 'checked_in_window'), '-')
|
|
assert.equal(state.tiers.value.by_config[0].config_id, 0)
|
|
assert.equal(state.packageRows.value.find((row) => row.state === 'unknown').cards, 0)
|
|
})
|
|
|
|
test('statistics page and separate area permissions are both required', async (t) => {
|
|
for (const permissions of [[], [perms.preReset, perms.tiers], [perms.statsPage]]) {
|
|
const { state, calls } = mount(t, statsPath, { permissions })
|
|
await state.loadPreReset()
|
|
await state.loadTiers()
|
|
assert.equal(
|
|
calls.filter((call) => ['getPreResetStats', 'getTierStats'].includes(call.name)).length,
|
|
0
|
|
)
|
|
}
|
|
})
|
|
|
|
test('new fields persist through whitelisted CRUD and menu/button contracts use distinct codes', () => {
|
|
const pageSource = source('src/views/polling-management/config/index.vue')
|
|
const body = helpers.pollingSaveBody(
|
|
{
|
|
...draft(),
|
|
asset_status: 2,
|
|
package_state: 'active',
|
|
pre_reset_window: 60,
|
|
pre_reset_carddata_interval: 60
|
|
},
|
|
false
|
|
)
|
|
for (const field of [
|
|
'asset_status',
|
|
'package_state',
|
|
'pre_reset_window',
|
|
'pre_reset_carddata_interval'
|
|
])
|
|
assert.ok(Object.hasOwn(body, field))
|
|
assert.match(pageSource, /pollingSaveBody\(formData/)
|
|
assert.match(pageSource, /:show-expand="searchFormItems.length >= 4"/)
|
|
assert.match(
|
|
source('src/router/routesAlias.ts'),
|
|
/PollingConfigStats = '\/polling-management\/config-stats'/
|
|
)
|
|
assert.equal(new Set(Object.values(perms)).size, 5)
|
|
for (const code of Object.values(perms)) assert.match(code, /^[a-z_]+:[a-z_]+$/)
|
|
})
|
|
|
|
test('interval field UI emits distinct null, zero and positive values', async (t) => {
|
|
const { descriptor } = parse(
|
|
source('src/views/polling-management/config/components/PollingIntervalField.vue')
|
|
)
|
|
const definition = evaluate(compileScript(descriptor, { id: 'polling-interval-test' }).content, {
|
|
vue
|
|
}).default
|
|
const value = vue.ref(null)
|
|
let state
|
|
const component = {
|
|
...definition,
|
|
setup(props, context) {
|
|
state = definition.setup(props, context)
|
|
return () => null
|
|
}
|
|
}
|
|
const renderer = vue.createRenderer({
|
|
createComment: () => ({}),
|
|
insert() {},
|
|
remove() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null
|
|
})
|
|
const app = renderer.createApp({
|
|
render: () =>
|
|
vue.h(component, {
|
|
min: 30,
|
|
modelValue: value.value,
|
|
'onUpdate:modelValue': (next) => {
|
|
value.value = next
|
|
}
|
|
})
|
|
})
|
|
app.mount({})
|
|
t.after(() => app.unmount())
|
|
assert.equal(state.mode.value, 'inherit')
|
|
state.mode.value = 'disabled'
|
|
await vue.nextTick()
|
|
assert.equal(value.value, 0)
|
|
state.mode.value = 'interval'
|
|
await vue.nextTick()
|
|
assert.equal(value.value, 30)
|
|
state.mode.value = 'inherit'
|
|
await vue.nextTick()
|
|
assert.equal(value.value, null)
|
|
})
|
|
|
|
test('progress errors stop automatic polling and do not create a new task', async (t) => {
|
|
const clock = fakeTimers()
|
|
const { state, calls } = mount(t, 'progress', {
|
|
permissions: [perms.progress],
|
|
globals: clock.globals,
|
|
overrides: {
|
|
getRecalculateTask: () => {
|
|
throw new Error('任务查询失败')
|
|
}
|
|
}
|
|
})
|
|
state.taskId.value = 'task-1'
|
|
state.autoRefresh.value = true
|
|
await state.query()
|
|
assert.match(state.error.value, /任务查询失败/)
|
|
assert.equal(clock.timers.size, 0)
|
|
assert.equal(writes(calls).length, 0)
|
|
})
|
|
|
|
test('partial or duplicate configuration pages cannot be submitted as a complete priority order', async (t) => {
|
|
const { state, calls } = mount(t, priorityPath, {
|
|
superAdmin: true,
|
|
overrides: {
|
|
getPollingConfigs: () =>
|
|
ok({ items: [{ id: 1, config_name: '重复配置', priority: 1 }], total: 2 })
|
|
}
|
|
})
|
|
await flush()
|
|
assert.equal(state.loaded.value, false)
|
|
assert.match(state.error.value, /不完整或存在重复/)
|
|
await state.save()
|
|
assert.equal(writes(calls).length, 0)
|
|
})
|
|
|
|
test('pre-reset draft controls require an explicit enable and preserve null when disabled', async (t) => {
|
|
const { descriptor } = parse(
|
|
source('src/views/polling-management/config/components/PollingDraftFields.vue')
|
|
)
|
|
const definition = evaluate(compileScript(descriptor, { id: 'polling-window-test' }).content, {
|
|
vue,
|
|
'@/config/constants/pollingConfig': constants,
|
|
'@/composables/useGateway': {
|
|
useGatewayCarriers: () => ({
|
|
options: vue.ref([]),
|
|
loading: vue.ref(false),
|
|
error: vue.ref(''),
|
|
search() {},
|
|
remember() {},
|
|
select() {}
|
|
})
|
|
},
|
|
'./PollingIntervalField.vue': { default: {} }
|
|
}).default
|
|
const value = vue.ref(draft())
|
|
let state
|
|
const component = {
|
|
...definition,
|
|
setup(props, context) {
|
|
state = definition.setup(props, context)
|
|
return () => null
|
|
}
|
|
}
|
|
const renderer = vue.createRenderer({
|
|
createComment: () => ({}),
|
|
insert() {},
|
|
remove() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null
|
|
})
|
|
const app = renderer.createApp({
|
|
render: () =>
|
|
vue.h(component, {
|
|
modelValue: value.value,
|
|
'onUpdate:modelValue': (next) => {
|
|
value.value = next
|
|
}
|
|
})
|
|
})
|
|
app.mount({})
|
|
t.after(() => app.unmount())
|
|
assert.equal(state.preResetEnabled.value, false)
|
|
assert.equal(value.value.pre_reset_window, null)
|
|
state.preResetEnabled.value = true
|
|
await vue.nextTick()
|
|
assert.equal(value.value.pre_reset_window, 60)
|
|
assert.equal(value.value.pre_reset_carddata_interval, 60)
|
|
state.preResetEnabled.value = false
|
|
await vue.nextTick()
|
|
assert.equal(value.value.pre_reset_window, null)
|
|
assert.equal(value.value.pre_reset_carddata_interval, null)
|
|
})
|
|
|
|
test('closing a preview or progress query while pending discards its response', async (t) => {
|
|
for (const kind of ['preview', 'progress']) {
|
|
const pending = deferred()
|
|
const { state, unmount } = mount(t, kind === 'preview' ? previewPath : 'progress', {
|
|
permissions: [perms.preview, perms.progress],
|
|
overrides:
|
|
kind === 'preview'
|
|
? { previewMatch: () => pending.promise }
|
|
: { getRecalculateTask: () => pending.promise }
|
|
})
|
|
if (kind === 'preview') state.iccid.value = 'CARD'
|
|
else state.taskId.value = 'task-1'
|
|
const query = kind === 'preview' ? state.runPreview() : state.query()
|
|
unmount()
|
|
pending.resolve(ok(kind === 'preview' ? preview : task()))
|
|
await query
|
|
assert.equal(kind === 'preview' ? state.result.value : state.task.value, null)
|
|
}
|
|
})
|