633 lines
26 KiB
JavaScript
633 lines
26 KiB
JavaScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFile } from 'node:fs/promises'
|
||
import { resolve } from 'node:path'
|
||
import { createRequire } from 'node:module'
|
||
import ts from 'typescript'
|
||
import * as vue from 'vue'
|
||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||
|
||
const require = createRequire(import.meta.url)
|
||
const esbuild = createRequire(require.resolve('vite'))('esbuild')
|
||
|
||
async function loadModule(entry, stubService = false) {
|
||
const result = await esbuild.build({
|
||
entryPoints: [entry],
|
||
bundle: true,
|
||
write: false,
|
||
format: 'cjs',
|
||
platform: 'node',
|
||
alias: { '@': resolve('src') },
|
||
plugins: stubService
|
||
? [
|
||
{
|
||
name: 'stub-base-service',
|
||
setup(build) {
|
||
build.onResolve({ filter: /BaseService$/ }, () => ({
|
||
path: 'base-service',
|
||
namespace: 'stub'
|
||
}))
|
||
build.onLoad({ filter: /.*/, namespace: 'stub' }, () => ({
|
||
contents: `
|
||
export class BaseService {
|
||
static calls = [];
|
||
static put(url, data, config) { this.calls.push({url, data, config}); return Promise.resolve({code:0, data:{}}); }
|
||
static update(url, data) { return this.put(url, data); }
|
||
static post(url, data, config) { this.calls.push({url, data, config}); return Promise.resolve({code:0, data:{}}); }
|
||
}`
|
||
}))
|
||
}
|
||
}
|
||
]
|
||
: []
|
||
})
|
||
const module = { exports: {} }
|
||
new Function('require', 'module', 'exports', result.outputFiles[0].text)(
|
||
require,
|
||
module,
|
||
module.exports
|
||
)
|
||
return module.exports
|
||
}
|
||
|
||
test('new login password allows special characters and requires at least three of four classes', async () => {
|
||
const { getNewLoginPasswordError, createNewLoginPasswordRule } = await loadModule(
|
||
'src/utils/validation/loginPassword.ts'
|
||
)
|
||
for (const password of [
|
||
'Abc12345',
|
||
'Abc123456789',
|
||
'Abc1234!',
|
||
'Abc1234_',
|
||
'ABC1234!',
|
||
'abc1234!',
|
||
'Abcdefg!',
|
||
'Ab1~`-=[]{}',
|
||
'Ab1;:\\|,<>/?'
|
||
])
|
||
assert.equal(getNewLoginPasswordError(password), undefined)
|
||
for (const password of [
|
||
'Abc1234',
|
||
'Abc123456789X',
|
||
'abcd1234',
|
||
'ABCD1234',
|
||
'AbcDefGh',
|
||
'ABCDEF!!',
|
||
'abcdef!!',
|
||
'123456!!',
|
||
'Abc1234 ',
|
||
' Abc12345',
|
||
'Abc1234\n',
|
||
'Abc1234中',
|
||
'Abc123😀',
|
||
'Abc12345'
|
||
]) {
|
||
assert.ok(getNewLoginPasswordError(password), `must reject ${JSON.stringify(password)}`)
|
||
}
|
||
const rule = createNewLoginPasswordRule()
|
||
for (const value of ['', undefined, null])
|
||
rule.validator({}, value, (error) => assert.equal(error, undefined))
|
||
rule.validator({}, 'Abc1234!', (error) => assert.equal(error, undefined))
|
||
rule.validator({}, 'abcdef!!', (error) => assert.ok(error instanceof Error))
|
||
for (const symbol of Array.from('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~')) {
|
||
assert.equal(getNewLoginPasswordError(`Ab12345${symbol}`), undefined)
|
||
}
|
||
})
|
||
|
||
test('admin and scan-registration new-password forms share validation, hints and the 12-character input limit', async () => {
|
||
const forms = [
|
||
{
|
||
path: 'src/components/core/layouts/art-header-bar/ChangePasswordDialog.vue',
|
||
models: ['formData.new_password', 'formData.confirm_password'],
|
||
fields: ['new_password', 'confirm_password'],
|
||
hint: "t('passwordChange.passwordRule')"
|
||
},
|
||
{
|
||
path: 'src/views/account-management/account/index.vue',
|
||
models: ['formData.password'],
|
||
fields: ['password']
|
||
},
|
||
{
|
||
path: 'src/views/common/account-list.vue',
|
||
models: ['accountForm.password'],
|
||
fields: ['password']
|
||
},
|
||
{
|
||
path: 'src/views/shop-management/list/index.vue',
|
||
models: ['formData.init_password'],
|
||
fields: ['init_password']
|
||
},
|
||
{
|
||
path: 'src/views/account-management/enterprise-customer/index.vue',
|
||
models: ['form.password', 'passwordForm.password'],
|
||
fields: ['password', 'password']
|
||
},
|
||
{
|
||
path: 'src/views/agent-registration/index.vue',
|
||
models: ['form.password'],
|
||
fields: ['password'],
|
||
hint: null
|
||
}
|
||
]
|
||
for (const { path, models, fields, hint = 'NEW_LOGIN_PASSWORD_HINT' } of forms) {
|
||
const source = await readFile(path, 'utf8')
|
||
assert.match(source, /from '@\/utils\/validation\/loginPassword'/, path)
|
||
const inputs = [...source.matchAll(/<ElInput\b[\s\S]*?\/>/g)].map(([input]) => input)
|
||
for (const model of models) {
|
||
// Exact v-model also rejects a silent .trim modifier on password inputs.
|
||
const matches = inputs.filter((input) => input.includes(`v-model="${model}"`))
|
||
assert.equal(matches.length, 1, `${path}: ${model}`)
|
||
assert.match(matches[0], /type="password"/, `${path}: ${model}`)
|
||
assert.match(matches[0], /maxlength="12"/, `${path}: ${model}`)
|
||
if (hint) {
|
||
assert.ok(matches[0].includes(`:placeholder="${hint}"`), `${path}: ${model} hint`)
|
||
} else {
|
||
assert.match(matches[0], /aria-describedby="registration-password-rule"/, path)
|
||
assert.match(source, /id="registration-password-rule"[\s\S]*?NEW_LOGIN_PASSWORD_HINT/, path)
|
||
}
|
||
}
|
||
const rules = [
|
||
...source.matchAll(
|
||
/\b(password|new_password|confirm_password|init_password):\s*\[([\s\S]*?)\n\s*\]/g
|
||
)
|
||
]
|
||
assert.deepEqual(rules.map(([, field]) => field).sort(), [...fields].sort(), path)
|
||
for (const [, field, rule] of rules) {
|
||
assert.match(rule, /required: true/, `${path}: ${field}`)
|
||
assert.match(rule, /createNewLoginPasswordRule\(/, `${path}: ${field}`)
|
||
assert.doesNotMatch(rule, /\b(?:min|max|pattern):/, `${path}: ${field} stale rule`)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('scan registration validates password and locally handles errors without changing public endpoint, payload or SMS behavior', async () => {
|
||
const { AgentDistributionService } = await loadModule(
|
||
'src/api/modules/agentDistribution.ts',
|
||
true
|
||
)
|
||
AgentDistributionService.calls = []
|
||
const params = {
|
||
distribution_code: 'local-test',
|
||
phone: '13800000000',
|
||
code: '123456',
|
||
shop_name: '本地测试店铺',
|
||
shop_code: 'LOCAL_TEST',
|
||
username: 'local_test'
|
||
}
|
||
for (const password of ['Abc12345', 'Abc123456789', 'ABC1234!', 'abc1234!', 'Abcdefg!']) {
|
||
await AgentDistributionService.registerAgent({ ...params, password })
|
||
assert.deepEqual(AgentDistributionService.calls.at(-1), {
|
||
url: '/api/c/v1/agent-distribution-registrations',
|
||
data: { ...params, password },
|
||
config: { requestOptions: { withToken: false, errorMessageMode: 'none' } }
|
||
})
|
||
}
|
||
for (const password of ['', 'Abc1234', 'Abc123456789X', 'abcd1234', 'Abc1234 ', 'Abc1234中']) {
|
||
await assert.rejects(AgentDistributionService.registerAgent({ ...params, password }))
|
||
}
|
||
assert.equal(AgentDistributionService.calls.length, 5)
|
||
await AgentDistributionService.sendCode({ phone: params.phone, scene: 'bind_phone' })
|
||
assert.deepEqual(AgentDistributionService.calls.at(-1), {
|
||
url: '/api/c/v1/auth/send-code',
|
||
data: { phone: params.phone, scene: 'bind_phone' },
|
||
config: { requestOptions: { withToken: false } }
|
||
})
|
||
})
|
||
|
||
test('scan registration masks template errors in both business and HTTP responses and uses only local feedback', async () => {
|
||
const { getAgentRegistrationFailureMessage } = await loadModule(
|
||
'src/utils/business/agentRegistrationError.ts'
|
||
)
|
||
for (const code of [1050, 1001, '1050', '1001']) {
|
||
const failure = { code, data: null, msg: '企业微信审批模板无效或不可访问' }
|
||
for (const value of [
|
||
failure,
|
||
{ ...failure, data: { detail: 'internal template settings' } },
|
||
{ isAxiosError: true, response: { status: 400, data: failure } },
|
||
{ isAxiosError: true, response: { status: 500, data: failure } },
|
||
{ data: failure }
|
||
]) {
|
||
assert.equal(getAgentRegistrationFailureMessage(value), '注册失败, 请联系管理员')
|
||
}
|
||
}
|
||
assert.equal(getAgentRegistrationFailureMessage({ code: 1203, msg: '验证码错误' }), '验证码错误')
|
||
assert.equal(
|
||
getAgentRegistrationFailureMessage({ code: 1204, message: '分销码不可用' }),
|
||
'分销码不可用'
|
||
)
|
||
assert.equal(
|
||
getAgentRegistrationFailureMessage({ isAxiosError: true, code: 'ECONNABORTED' }),
|
||
'请求超时,结果可能未知,请刷新确认'
|
||
)
|
||
for (const value of [null, undefined, new Error('network')]) {
|
||
assert.equal(typeof getAgentRegistrationFailureMessage(value), 'string')
|
||
}
|
||
const page = await readFile('src/views/agent-registration/index.vue', 'utf8')
|
||
assert.match(page, /ElMessage.error\(getAgentRegistrationFailureMessage\(res\)\)/)
|
||
assert.match(
|
||
page,
|
||
/catch \(error\)\s*\{\s*ElMessage.error\(getAgentRegistrationFailureMessage\(error\)\)/
|
||
)
|
||
assert.doesNotMatch(page, /ElMessage.error\('分销码不可用'\)/)
|
||
assert.match(page, /if \(!valid \|\| submitting.value\) return/)
|
||
})
|
||
|
||
test('scan registration emits exactly one masked message per template failure and retains the form for retry', async () => {
|
||
const { getAgentRegistrationFailureMessage } = await loadModule(
|
||
'src/utils/business/agentRegistrationError.ts'
|
||
)
|
||
const passwordValidation = await loadModule('src/utils/validation/loginPassword.ts')
|
||
const { descriptor } = parse(await readFile('src/views/agent-registration/index.vue', 'utf8'))
|
||
const script = compileScript(descriptor, { id: 'scan-registration-error-test' }).content
|
||
const compiled = ts.transpileModule(script, {
|
||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||
}).outputText
|
||
for (const code of [1050, 1001]) {
|
||
for (const rejected of [false, true]) {
|
||
const messages = []
|
||
const failure = { code, data: null, msg: '企业微信审批模板无效或不可访问' }
|
||
const imports = {
|
||
vue: { ...vue, onBeforeUnmount() {} },
|
||
'vue-router': { useRoute: () => ({ query: {} }) },
|
||
'element-plus': { ElMessage: { error: (message) => messages.push(message) } },
|
||
'@/api/modules/agentDistribution': {
|
||
AgentDistributionService: {
|
||
registerAgent: async () => {
|
||
if (rejected) throw { isAxiosError: true, response: { status: 400, data: failure } }
|
||
return failure
|
||
}
|
||
}
|
||
},
|
||
'@/utils/constants/regionData': { regionData: [] },
|
||
'@/utils/validation/loginPassword': passwordValidation,
|
||
'@/utils/business/agentRegistrationError': { getAgentRegistrationFailureMessage }
|
||
}
|
||
const module = { exports: {} }
|
||
new Function('require', 'module', 'exports', compiled)(
|
||
(name) => {
|
||
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
||
return imports[name]
|
||
},
|
||
module,
|
||
module.exports
|
||
)
|
||
const state = module.exports.default.setup({}, { expose() {} })
|
||
Object.assign(state.form, { distribution_code: 'local-test', password: 'Abc1234!' })
|
||
let submission
|
||
state.formRef.value = {
|
||
validate: (callback) => {
|
||
submission = callback(true)
|
||
}
|
||
}
|
||
state.handleSubmit()
|
||
state.handleSubmit()
|
||
await submission
|
||
assert.deepEqual(messages, ['注册失败, 请联系管理员'])
|
||
assert.equal(state.submitted.value, false)
|
||
assert.equal(state.submitting.value, false)
|
||
assert.equal(state.form.distribution_code, 'local-test')
|
||
assert.equal(state.form.password, 'Abc1234!')
|
||
}
|
||
}
|
||
})
|
||
|
||
test('scan registration password feedback uses the shared rule rather than treating spaces or Chinese as symbols', async () => {
|
||
const page = await readFile('src/views/agent-registration/index.vue', 'utf8')
|
||
assert.match(page, /return getNewLoginPasswordError\(pwd\) \? 1 : 3/)
|
||
assert.match(page, /密码符合规则/)
|
||
assert.match(page, /aria-live="polite"/)
|
||
assert.match(page, /password-form-item :deep\(\.el-form-item__error\)[\s\S]*?position: static/)
|
||
assert.doesNotMatch(page, /8-32|maxlength="32"/)
|
||
assert.match(page, /password: form.password,/)
|
||
})
|
||
|
||
test('account password requests use the right field and omit unchanged optional password', async () => {
|
||
const { AccountService } = await loadModule('src/api/modules/account.ts', true)
|
||
AccountService.calls = []
|
||
await AccountService.updateAccountPassword(12, 'Abc12345')
|
||
assert.deepEqual(AccountService.calls[0].data, { new_password: 'Abc12345' })
|
||
await AccountService.updateAccount(12, { username: 'operator', password: '' })
|
||
assert.deepEqual(AccountService.calls[1].data, { username: 'operator' })
|
||
await AccountService.updateAccount(12, { password: 'Abc1234!' })
|
||
assert.deepEqual(AccountService.calls[2].data, { password: 'Abc1234!' })
|
||
await AccountService.updateAccountPassword(12, 'ABC1234!')
|
||
assert.deepEqual(AccountService.calls[3].data, { new_password: 'ABC1234!' })
|
||
await assert.rejects(AccountService.updateAccount(12, { password: 'abcdef!!' }))
|
||
await assert.rejects(AccountService.updateAccountPassword(12, 'abcd1234'))
|
||
assert.equal(AccountService.calls.length, 4)
|
||
})
|
||
|
||
test('change-password dialog is appended to body so its overlay covers the sidebar stacking context', async () => {
|
||
const dialog = await readFile(
|
||
'src/components/core/layouts/art-header-bar/ChangePasswordDialog.vue',
|
||
'utf8'
|
||
)
|
||
assert.match(dialog, /append-to-body/)
|
||
assert.match(dialog, /createNewLoginPasswordRule\(t\('passwordChange.passwordRule'\)\)/)
|
||
for (const language of ['zh', 'en']) {
|
||
const locale = JSON.parse(await readFile(`src/locales/langs/${language}.json`, 'utf8'))
|
||
assert.doesNotMatch(locale.passwordChange.passwordRule, /不允许特殊符号|no special characters/)
|
||
}
|
||
})
|
||
|
||
test('writeoff terminal failure blocks recovery but allows resubmission; unknown result stays recoverable', async () => {
|
||
const module = await loadModule(
|
||
'src/views/finance/employee-collection/employeeCollectionDisplay.ts'
|
||
)
|
||
assert.equal(module.canRecoverApplicationApproval({ status: 4, recoverable: true }), false)
|
||
assert.equal(module.canResubmitApplication({ status: 4 }), true)
|
||
assert.equal(
|
||
module.canRecoverApplicationApproval({
|
||
status: 0,
|
||
approval_submission_status: 4,
|
||
recoverable: true
|
||
}),
|
||
true
|
||
)
|
||
assert.equal(module.canRecoverApplicationApproval({ status: 0, recoverable: false }), false)
|
||
assert.equal(module.getApplicationStatusTagType(4), 'danger')
|
||
for (const [value, label] of [
|
||
[0, '待提交'],
|
||
[1, '提交中'],
|
||
[2, '已提交'],
|
||
[3, '提交失败'],
|
||
[4, '结果未知']
|
||
]) {
|
||
assert.equal(module.getApplicationSubmissionLabel({ approval_submission_status: value }), label)
|
||
}
|
||
})
|
||
|
||
test('refund submission failure uses resubmit, not recovery, without changing status 6 anomaly handling', async () => {
|
||
const module = await loadModule('src/config/constants/refund.ts')
|
||
assert.equal(module.canRecoverRefundApproval({ status: 7, approval_recoverable: true }), false)
|
||
assert.equal(module.canResubmitRefund({ status: 7 }), true)
|
||
assert.equal(module.canResubmitRefund({ status: 3 }), true)
|
||
assert.equal(
|
||
module.canRecoverRefundApproval({
|
||
status: 1,
|
||
submission_status: 4,
|
||
approval_recoverable: true
|
||
}),
|
||
true
|
||
)
|
||
assert.equal(module.canResubmitRefund({ status: 6, anomaly_flag: 1 }), false)
|
||
assert.equal(module.canResubmitRefund({ status: 6, anomaly_flag: 0 }), true)
|
||
assert.equal(module.RefundStatusNameMap[7], '审批提交失败')
|
||
})
|
||
|
||
test('admin agent registration does not recover terminal failures and provides status fallback', async () => {
|
||
const module = await loadModule('src/types/api/agentDistributionRegistration.ts')
|
||
assert.equal(module.canRecoverAgentRegistration({ status: 3, recoverable: true }), false)
|
||
assert.equal(module.canRecoverAgentRegistration({ status: 0, recoverable: true }), true)
|
||
assert.equal(module.getAgentRegistrationStatusName({ status: 3 }), '审批提交失败')
|
||
})
|
||
|
||
test('offline recharge failure keeps read actions and starts a new order only with existing role/permission', async () => {
|
||
const { buildAgentRechargeActions } = await loadModule(
|
||
'src/views/finance/agent-recharge/agentRechargeActions.ts'
|
||
)
|
||
const row = { id: 8, status: 7, payment_method: 'offline', payment_voucher_key: ['voucher'] }
|
||
let restarted = false
|
||
const options = {
|
||
hasAuth: () => true,
|
||
canCreateOffline: true,
|
||
onRestart: () => {
|
||
restarted = true
|
||
},
|
||
onViewPaymentVoucher() {},
|
||
onConfirmPayment() {},
|
||
onReject() {},
|
||
onTriggerApproval() {}
|
||
}
|
||
const actions = buildAgentRechargeActions(row, options)
|
||
assert.deepEqual(
|
||
actions.map((action) => action.label),
|
||
['支付凭证', '重新发起']
|
||
)
|
||
actions[1].handler()
|
||
assert.equal(restarted, true)
|
||
assert.deepEqual(
|
||
buildAgentRechargeActions(row, { ...options, canCreateOffline: false }).map(
|
||
(action) => action.label
|
||
),
|
||
['支付凭证']
|
||
)
|
||
assert.deepEqual(buildAgentRechargeActions(row, { ...options, hasAuth: () => false }), [])
|
||
})
|
||
|
||
test('withdrawal failure is independently typed from commission clawback and supports resubmit', async () => {
|
||
const module = await loadModule('src/config/constants/commission.ts')
|
||
assert.equal(module.WithdrawalStatusMap[5].label, '审批提交失败')
|
||
assert.equal(module.canResubmitWithdrawal(5), true)
|
||
assert.equal(module.canResubmitWithdrawal(3), true)
|
||
assert.equal(module.canResubmitWithdrawal(1), false)
|
||
assert.equal(module.getCommissionStatusLabel(5), '回溯')
|
||
assert.equal(
|
||
module.WITHDRAWAL_QUALIFICATION_STATUS_OPTIONS.find((item) => item.value === 4).label,
|
||
'审批提交失败'
|
||
)
|
||
})
|
||
|
||
test('order payment record status is independent from order status and channels preserve unknown codes', async () => {
|
||
const module = await loadModule('src/utils/business/orderPayment.ts')
|
||
assert.equal(module.getOrderPaymentRecordStatusName(0), '待支付')
|
||
assert.equal(module.getOrderPaymentRecordStatusName(1), '已支付')
|
||
assert.equal(module.getOrderPaymentRecordStatusName(2), '已失败')
|
||
assert.equal(module.getOrderPaymentRecordStatusName(3), '已退款')
|
||
assert.equal(module.getOrderPaymentRecordTagType(2), 'danger')
|
||
assert.equal(module.getOrderPaymentChannelName('wechat'), '微信直连 v3')
|
||
assert.equal(module.getOrderPaymentChannelName('wechat_v2'), '微信直连 v2')
|
||
assert.equal(module.getOrderPaymentChannelName('fuiou'), '富友')
|
||
assert.equal(module.getOrderPaymentChannelName(''), '-')
|
||
assert.equal(module.getOrderPaymentChannelName('future-channel'), 'future-channel')
|
||
})
|
||
|
||
test('exchange phone conflicts support HTTP error and business response without inventing a missing number', async () => {
|
||
const module = await loadModule('src/utils/business/exchangeError.ts')
|
||
const failure = {
|
||
code: 1202,
|
||
data: { phone: '13800000000' },
|
||
msg: '新资产已绑定手机号,请先在后台解除该资产的手机号绑定,再重新提交换货申请'
|
||
}
|
||
assert.equal(module.getExchangePhoneConflictMessage(failure), failure.msg)
|
||
assert.equal(
|
||
module.getExchangePhoneConflictMessage({ response: { data: failure } }),
|
||
module.getExchangePhoneConflictMessage(failure)
|
||
)
|
||
assert.match(module.getExchangePhoneConflictMessage({ code: 1202 }), /换货申请未完成/)
|
||
assert.doesNotMatch(module.getExchangePhoneConflictMessage({ code: 1202 }), /undefined|null/)
|
||
assert.equal(module.getExchangePhoneConflictMessage({ code: 1206 }), undefined)
|
||
assert.equal(
|
||
module.getExchangeBusinessErrorMessage({ response: { data: { message: '迁移失败' } } }),
|
||
'迁移失败'
|
||
)
|
||
const { ExchangeService } = await loadModule('src/api/modules/exchange.ts', true)
|
||
ExchangeService.calls = []
|
||
const config = { requestOptions: { errorMessageMode: 'none' } }
|
||
await ExchangeService.completeExchange(9, config)
|
||
assert.deepEqual(ExchangeService.calls[0], {
|
||
url: '/api/admin/exchanges/9/complete',
|
||
data: {},
|
||
config
|
||
})
|
||
})
|
||
|
||
test('exchange association handoff carries only the new asset and its bound phone as strings', async () => {
|
||
const { getExchangePhoneAssociationQuery } = await loadModule(
|
||
'src/utils/business/exchangeError.ts'
|
||
)
|
||
const newAsset = { asset_identifier: '89860624590014594318', associated_phones: ['19232713115'] }
|
||
assert.deepEqual(
|
||
getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, newAsset),
|
||
{
|
||
asset_identifier: '89860624590014594318',
|
||
phone: '19232713115'
|
||
}
|
||
)
|
||
assert.equal(
|
||
getExchangePhoneAssociationQuery({ response: { data: { code: 1202 } } }, newAsset),
|
||
undefined
|
||
)
|
||
assert.equal(getExchangePhoneAssociationQuery({ code: 1206 }, newAsset), undefined)
|
||
assert.equal(
|
||
getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, {}),
|
||
undefined
|
||
)
|
||
assert.equal(
|
||
getExchangePhoneAssociationQuery(
|
||
{ code: 1202 },
|
||
{ asset_identifier: '89860624590014594318', associated_phones: [] }
|
||
),
|
||
undefined
|
||
)
|
||
assert.deepEqual(
|
||
getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '13900000000' } }, newAsset),
|
||
{
|
||
asset_identifier: '89860624590014594318',
|
||
phone: '13900000000'
|
||
}
|
||
)
|
||
})
|
||
|
||
test('1202 with data null keeps the original msg and cannot navigate to unbind despite asset phone history', async () => {
|
||
const { getExchangePhoneConflictMessage, getExchangePhoneAssociationQuery } = await loadModule(
|
||
'src/utils/business/exchangeError.ts'
|
||
)
|
||
const failure = {
|
||
code: 1202,
|
||
data: null,
|
||
msg: '该资产已完成过换货,如需再次换货请使用换货后的新资产'
|
||
}
|
||
const newAsset = { asset_identifier: '89860624590014594318', associated_phones: ['19232713115'] }
|
||
for (const value of [failure, { response: { data: failure } }]) {
|
||
assert.equal(getExchangePhoneConflictMessage(value), failure.msg)
|
||
assert.equal(getExchangePhoneAssociationQuery(value, newAsset), undefined)
|
||
}
|
||
for (const phone of ['', ' ', null, 19232713115]) {
|
||
assert.equal(
|
||
getExchangePhoneAssociationQuery({ ...failure, data: { phone } }, newAsset),
|
||
undefined
|
||
)
|
||
}
|
||
const page = await readFile('src/views/asset-management/exchange-management/index.vue', 'utf8')
|
||
assert.match(page, /if \(!canManageAssociations\)/)
|
||
assert.match(page, /ElMessageBox.alert\(conflictMessage, '换货提示'/)
|
||
})
|
||
|
||
test('association preselection excludes old assets, different phones and invalid historical rows', async () => {
|
||
const {
|
||
getPhoneAssetAssociationTarget,
|
||
getMatchingPhoneAssetAssociations,
|
||
getPhoneAssetUnbindResultName
|
||
} = await loadModule('src/utils/business/phoneAssetAssociation.ts')
|
||
const target = getPhoneAssetAssociationTarget({
|
||
asset_identifier: ' 89860624590014594318 ',
|
||
phone: '19232713115'
|
||
})
|
||
const rows = [
|
||
{ id: 1, asset_identifier: target.asset_identifier, phone: target.phone, status: 1 },
|
||
{ id: 2, asset_identifier: '89860625770070732445', phone: target.phone, status: 1 },
|
||
{ id: 3, asset_identifier: target.asset_identifier, phone: '13900000000', status: 1 },
|
||
{ id: 4, asset_identifier: target.asset_identifier, phone: target.phone, status: 0 }
|
||
]
|
||
assert.deepEqual(
|
||
getMatchingPhoneAssetAssociations(rows, target).map((row) => row.id),
|
||
[1]
|
||
)
|
||
assert.deepEqual(getMatchingPhoneAssetAssociations(rows, { ...target, phone: '' }), [])
|
||
assert.deepEqual(
|
||
getPhoneAssetAssociationTarget({ asset_identifier: ['a', 'b'], phone: ['19232713115'] }),
|
||
{ asset_identifier: '', phone: '' }
|
||
)
|
||
assert.equal(getPhoneAssetUnbindResultName(true), '解绑成功')
|
||
assert.equal(getPhoneAssetUnbindResultName(false), '解绑失败')
|
||
})
|
||
|
||
test('association page watches handoff changes, clears stale selections and retains manual unbind confirmation', async () => {
|
||
const page = await readFile(
|
||
'src/views/asset-management/phone-asset-association/index.vue',
|
||
'utf8'
|
||
)
|
||
assert.match(page, /getPhoneAssetAssociationTarget\(route.query\)/)
|
||
assert.match(page, /toggleRowSelection\(row, true\)/)
|
||
assert.match(page, /requestId !== listRequestId/)
|
||
assert.match(page, /getPhoneAssetUnbindResultName\(row.success\)/)
|
||
assert.doesNotMatch(page, /prop="success"/)
|
||
assert.match(page, /batchForm.confirmed = false/)
|
||
assert.match(page, /batchFormRef.value\?\.validate/)
|
||
const table = await readFile('src/components/core/tables/ArtTable.vue', 'utf8')
|
||
assert.match(table, /clearSelection:.*clearSelection\(\)/)
|
||
})
|
||
|
||
test('password validation errors accept backend message as well as msg', async () => {
|
||
const { normalizeApiError } = await loadModule('src/utils/business/apiError.ts')
|
||
for (const field of ['msg', 'message']) {
|
||
assert.equal(
|
||
normalizeApiError({
|
||
isAxiosError: true,
|
||
response: { status: 400, data: { [field]: '新密码不合法' } }
|
||
}).message,
|
||
'新密码不合法'
|
||
)
|
||
}
|
||
})
|
||
|
||
test('traffic snapshots preserve zero, missing values and MB precision', async () => {
|
||
const { formatTrafficAlertMb } = await loadModule('src/utils/business/trafficAlert.ts')
|
||
assert.equal(formatTrafficAlertMb(0), '0.00')
|
||
assert.equal(formatTrafficAlertMb(1234.567), '1234.57')
|
||
for (const value of [undefined, null, NaN]) assert.equal(formatTrafficAlertMb(value), '-')
|
||
})
|
||
|
||
test('CSV download has the new exact four-column header and preserves phone identifiers as text', async () => {
|
||
const { SHOP_BUSINESS_OWNER_CSV_TEMPLATE } = await loadModule(
|
||
'src/utils/business/shopBusinessOwnerImport.ts'
|
||
)
|
||
assert.equal(SHOP_BUSINESS_OWNER_CSV_TEMPLATE.charCodeAt(0), 0xfeff)
|
||
const [header, instruction] = SHOP_BUSINESS_OWNER_CSV_TEMPLATE.slice(1).trimEnd().split('\r\n')
|
||
assert.deepEqual(header.split(','), ['店铺编码', '操作类型', '平台登录账号', '备注'])
|
||
assert.equal(instruction.split(',').length, 4)
|
||
assert.match(instruction, /手机号/)
|
||
assert.match(instruction, /清空留空/)
|
||
assert.doesNotMatch(SHOP_BUSINESS_OWNER_CSV_TEMPLATE, /业务员登录账号/)
|
||
})
|
||
|
||
test('affected view contracts preserve raw payment history, package quota and device template', async () => {
|
||
const order = await readFile('src/views/order-management/order-list/detail.vue', 'utf8')
|
||
assert.match(order, /detailData\.payments \|\| \[\]/)
|
||
assert.doesNotMatch(order, /payments\.(filter|sort)/)
|
||
const rules = await readFile('src/views/package-management/traffic-alert-rules/index.vue', 'utf8')
|
||
assert.match(rules, /item\.real_data_mb/)
|
||
assert.doesNotMatch(rules, /row\.real_data_mb|prop: 'real_data_mb'/)
|
||
const devices = await readFile(
|
||
'src/views/asset-management/task-management/device-task/index.vue',
|
||
'utf8'
|
||
)
|
||
assert.ok(
|
||
/独立的 IMEI\s+列/.test(devices),
|
||
'import instructions must require the standalone IMEI column'
|
||
)
|
||
assert.match(devices, /@\/template\/设备导入模板\.xlsx/)
|
||
assert.match(devices, /failed_items/)
|
||
})
|