买家从卡券/订单消息里的链接打开,看到物品图片与信息,填游戏账号即可领取。 - **路径式无 `#`**:新增独立 BrowserRouter 分支(照 `/redeem` 的做法), `isClaimSkinPath` 只认 `/claim/<token>` 两段精确匹配; 打手领取页仍是主站 HashRouter 的 `#/claim/:token`(其 pathname 为 `/`),两者互不影响 - **物品卡片**:商品图(映射不到或加载失败都降级为占位块,不出现破图)、 内部 SKU、数量 ×N、订单号、引流标题(灰色副信息) - **领取表单**:按接口下发的 `claimFields` 动态渲染(游戏账号/角色ID), 提交前二次确认让买家核对账号 - **状态**:待领取展示表单;已领取展示成功态(优先于 tokenStatus); token 失效/过期/关闭/已用分别给中文提示与联系客服引导 - 纯函数抽到 `utils/claim-skin.ts` 并补 20 个单测(路由判定、状态映射、字段校验、格式化)
204 lines
8.3 KiB
TypeScript
204 lines
8.3 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import test from 'node:test'
|
||
|
||
import {
|
||
DEFAULT_CLAIM_SKIN_FIELD,
|
||
formatClaimSkinAmount,
|
||
formatClaimSkinQuantity,
|
||
formatClaimSkinTime,
|
||
isClaimSkinPath,
|
||
resolveClaimSkinFields,
|
||
resolveClaimSkinGameAccount,
|
||
resolveClaimSkinStatusView,
|
||
resolveClaimSkinToken,
|
||
resolveClaimSkinTokenStatusView,
|
||
validateClaimSkinField,
|
||
validateClaimSkinFields,
|
||
} from '../src/utils/claim-skin.ts'
|
||
import type { ClaimSkinTokenStatus } from '../src/types/claim-skin.ts'
|
||
|
||
/** 故意构造后端未来可能新增的状态,验证未知状态不会崩 */
|
||
const FUTURE_TOKEN_STATUS = 'reviewing' as unknown as ClaimSkinTokenStatus
|
||
|
||
test('isClaimSkinPath:只认路径式 /claim/<token>,容忍结尾斜杠', () => {
|
||
assert.equal(isClaimSkinPath('/claim/abc123'), true)
|
||
assert.equal(isClaimSkinPath('/claim/abc123/'), true)
|
||
assert.equal(isClaimSkinPath('/claim/abc123//'), true)
|
||
})
|
||
|
||
test('isClaimSkinPath:/claim、/claim/、多段路径、其它路径都不算', () => {
|
||
assert.equal(isClaimSkinPath('/claim'), false)
|
||
assert.equal(isClaimSkinPath('/claim/'), false)
|
||
assert.equal(isClaimSkinPath('/claim/a/b'), false)
|
||
assert.equal(isClaimSkinPath('/redeem'), false)
|
||
assert.equal(isClaimSkinPath('/admin/claim/abc'), false)
|
||
assert.equal(isClaimSkinPath('/'), false)
|
||
assert.equal(isClaimSkinPath(''), false)
|
||
assert.equal(isClaimSkinPath(null), false)
|
||
assert.equal(isClaimSkinPath(undefined), false)
|
||
})
|
||
|
||
test('isClaimSkinPath:打手页是 hash 路由,pathname 为 / 或 hash 串时不命中', () => {
|
||
// 打手链接形如 https://<域名>/#/claim/<token>:pathname 是 '/',hash 才是路由
|
||
assert.equal(isClaimSkinPath('/'), false)
|
||
assert.equal(isClaimSkinPath('#/claim/abc123'), false)
|
||
assert.equal(isClaimSkinPath('/claim/abc123#/claim/other'), true)
|
||
})
|
||
|
||
test('resolveClaimSkinToken:取出并解码 token 片段', () => {
|
||
assert.equal(resolveClaimSkinToken('/claim/abc123'), 'abc123')
|
||
assert.equal(resolveClaimSkinToken('/claim/abc123/?x=1'), 'abc123')
|
||
assert.equal(resolveClaimSkinToken('/claim/a%2Fb'), 'a/b')
|
||
assert.equal(resolveClaimSkinToken('/claim/%E4%B8%AD%E6%96%87'), '中文')
|
||
// 非法百分号编码退回原始片段,交给后端判定
|
||
assert.equal(resolveClaimSkinToken('/claim/100%'), '100%')
|
||
assert.equal(resolveClaimSkinToken('/redeem'), '')
|
||
})
|
||
|
||
test('formatClaimSkinAmount:分转元保留两位小数', () => {
|
||
assert.equal(formatClaimSkinAmount(100), '¥1.00')
|
||
assert.equal(formatClaimSkinAmount(0), '¥0.00')
|
||
assert.equal(formatClaimSkinAmount(5), '¥0.05')
|
||
assert.equal(formatClaimSkinAmount(1990), '¥19.90')
|
||
assert.equal(formatClaimSkinAmount(undefined), '¥0.00')
|
||
assert.equal(formatClaimSkinAmount('abc'), '¥0.00')
|
||
})
|
||
|
||
test('formatClaimSkinTime:空值或非法时间返回空串,合法时间为 YYYY-MM-DD HH:mm', () => {
|
||
assert.equal(formatClaimSkinTime(''), '')
|
||
assert.equal(formatClaimSkinTime(null), '')
|
||
assert.equal(formatClaimSkinTime(' '), '')
|
||
assert.equal(formatClaimSkinTime('不是时间'), '')
|
||
assert.equal(formatClaimSkinTime('2026-10-09 13:18:20'), '2026-10-09 13:18')
|
||
assert.match(formatClaimSkinTime('2026-09-17T10:00:00'), /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
|
||
})
|
||
|
||
test('formatClaimSkinQuantity:正数展示 ×N,非法 / 非正数返回空串', () => {
|
||
assert.equal(formatClaimSkinQuantity(1), '×1')
|
||
assert.equal(formatClaimSkinQuantity(2), '×2')
|
||
assert.equal(formatClaimSkinQuantity('3'), '×3')
|
||
assert.equal(formatClaimSkinQuantity(2.9), '×2')
|
||
assert.equal(formatClaimSkinQuantity(0), '')
|
||
assert.equal(formatClaimSkinQuantity(-1), '')
|
||
assert.equal(formatClaimSkinQuantity('abc'), '')
|
||
assert.equal(formatClaimSkinQuantity(null), '')
|
||
})
|
||
|
||
test('resolveClaimSkinTokenStatusView:只有 active 可用且不带客服引导', () => {
|
||
const active = resolveClaimSkinTokenStatusView('active')
|
||
assert.equal(active.available, true)
|
||
assert.equal(active.tone, 'success')
|
||
assert.equal(active.contactTip, '')
|
||
assert.ok(active.title)
|
||
assert.ok(active.description)
|
||
})
|
||
|
||
test('resolveClaimSkinTokenStatusView:closed / revoked / expired / used 都不可用且引导联系客服', () => {
|
||
for (const status of ['closed', 'revoked', 'expired', 'used']) {
|
||
const view = resolveClaimSkinTokenStatusView(status)
|
||
assert.equal(view.available, false, `${status} 不应可用`)
|
||
assert.notEqual(view.tone, 'success')
|
||
assert.ok(view.title, `${status} 需要标题`)
|
||
assert.ok(view.description, `${status} 需要说明`)
|
||
assert.ok(view.contactTip, `${status} 需要联系客服引导`)
|
||
}
|
||
|
||
assert.equal(resolveClaimSkinTokenStatusView('used').tone, 'info')
|
||
assert.equal(resolveClaimSkinTokenStatusView('revoked').tone, 'error')
|
||
})
|
||
|
||
test('resolveClaimSkinTokenStatusView:未知状态回退为不可用的中性提示', () => {
|
||
const view = resolveClaimSkinTokenStatusView(FUTURE_TOKEN_STATUS)
|
||
assert.equal(view.available, false)
|
||
assert.equal(view.tone, 'info')
|
||
assert.equal(view.title, '领取链接不可用')
|
||
assert.ok(view.contactTip)
|
||
})
|
||
|
||
test('resolveClaimSkinStatusView:pending_claim 展示表单,submitted 展示成功态', () => {
|
||
const pending = resolveClaimSkinStatusView('pending_claim')
|
||
assert.equal(pending.showForm, true)
|
||
assert.equal(pending.tone, 'info')
|
||
|
||
const submitted = resolveClaimSkinStatusView('submitted')
|
||
assert.equal(submitted.showForm, false)
|
||
assert.equal(submitted.tone, 'success')
|
||
})
|
||
|
||
test('resolveClaimSkinStatusView:未知状态不展示表单', () => {
|
||
const view = resolveClaimSkinStatusView('reviewing')
|
||
assert.equal(view.showForm, false)
|
||
assert.equal(view.tone, 'warning')
|
||
assert.equal(view.title, '领取状态未知')
|
||
})
|
||
|
||
test('resolveClaimSkinFields:透传后端字段并补齐默认值', () => {
|
||
assert.deepEqual(
|
||
resolveClaimSkinFields([
|
||
{
|
||
key: 'gameAccount',
|
||
label: '游戏账号 / 角色ID',
|
||
placeholder: '请输入你的游戏账号(角色ID)',
|
||
required: true,
|
||
},
|
||
]),
|
||
[
|
||
{
|
||
key: 'gameAccount',
|
||
label: '游戏账号 / 角色ID',
|
||
placeholder: '请输入你的游戏账号(角色ID)',
|
||
required: true,
|
||
},
|
||
],
|
||
)
|
||
})
|
||
|
||
test('resolveClaimSkinFields:缺失 / 空数组 / 脏数据都能得到可用字段', () => {
|
||
assert.deepEqual(resolveClaimSkinFields([]), [DEFAULT_CLAIM_SKIN_FIELD])
|
||
assert.deepEqual(resolveClaimSkinFields(null), [DEFAULT_CLAIM_SKIN_FIELD])
|
||
assert.deepEqual(resolveClaimSkinFields(undefined), [DEFAULT_CLAIM_SKIN_FIELD])
|
||
|
||
const normalized = resolveClaimSkinFields([
|
||
{ key: ' ' },
|
||
{ key: 'uid', label: '', required: false },
|
||
])
|
||
assert.deepEqual(normalized, [
|
||
{ key: 'uid', label: 'uid', placeholder: '请输入uid', required: false },
|
||
])
|
||
})
|
||
|
||
test('validateClaimSkinField:必填空值报错,填好或选填时通过', () => {
|
||
const field = { key: 'gameAccount', label: '游戏账号 / 角色ID', required: true }
|
||
assert.equal(validateClaimSkinField(field, ''), '请输入游戏账号 / 角色ID')
|
||
assert.equal(validateClaimSkinField(field, ' '), '请输入游戏账号 / 角色ID')
|
||
assert.equal(validateClaimSkinField(field, undefined), '请输入游戏账号 / 角色ID')
|
||
assert.equal(validateClaimSkinField(field, '123456'), '')
|
||
assert.equal(validateClaimSkinField({ key: 'note', label: '备注', required: false }, ''), '')
|
||
})
|
||
|
||
test('validateClaimSkinFields:返回第一条错误,全部通过时为空串', () => {
|
||
const fields = [
|
||
{ key: 'gameAccount', label: '游戏账号 / 角色ID', required: true },
|
||
{ key: 'server', label: '区服', required: true },
|
||
]
|
||
|
||
assert.equal(validateClaimSkinFields(fields, {}), '请输入游戏账号 / 角色ID')
|
||
assert.equal(validateClaimSkinFields(fields, { gameAccount: '123456' }), '请输入区服')
|
||
assert.equal(validateClaimSkinFields(fields, { gameAccount: '123456', server: '亚服' }), '')
|
||
})
|
||
|
||
test('resolveClaimSkinGameAccount:优先取 gameAccount,否则退化为第一个字段', () => {
|
||
const fields = [
|
||
{ key: 'server', label: '区服' },
|
||
{ key: 'gameAccount', label: '游戏账号 / 角色ID' },
|
||
]
|
||
|
||
assert.equal(
|
||
resolveClaimSkinGameAccount(fields, { server: '亚服', gameAccount: ' 123456 ' }),
|
||
'123456',
|
||
)
|
||
assert.equal(resolveClaimSkinGameAccount([{ key: 'uid', label: 'UID' }], { uid: ' 987 ' }), '987')
|
||
assert.equal(resolveClaimSkinGameAccount([], { gameAccount: '123456' }), '')
|
||
assert.equal(resolveClaimSkinGameAccount(fields, null), '')
|
||
})
|