Files
order_site/apps/frontend/tests/claim-skin.test.ts
T
yml2213 eacca11768 feat(claim-skin): 买家皮肤领取页(路径式 /claim/<token>)
买家从卡券/订单消息里的链接打开,看到物品图片与信息,填游戏账号即可领取。

- **路径式无 `#`**:新增独立 BrowserRouter 分支(照 `/redeem` 的做法),
  `isClaimSkinPath` 只认 `/claim/<token>` 两段精确匹配;
  打手领取页仍是主站 HashRouter 的 `#/claim/:token`(其 pathname 为 `/`),两者互不影响
- **物品卡片**:商品图(映射不到或加载失败都降级为占位块,不出现破图)、
  内部 SKU、数量 ×N、订单号、引流标题(灰色副信息)
- **领取表单**:按接口下发的 `claimFields` 动态渲染(游戏账号/角色ID),
  提交前二次确认让买家核对账号
- **状态**:待领取展示表单;已领取展示成功态(优先于 tokenStatus);
  token 失效/过期/关闭/已用分别给中文提示与联系客服引导
- 纯函数抽到 `utils/claim-skin.ts` 并补 20 个单测(路由判定、状态映射、字段校验、格式化)
2026-10-09 15:06:56 +08:00

204 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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), '')
})