291 lines
10 KiB
TypeScript
291 lines
10 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
import {
|
|
evaluateWorkerKeywordVerdict,
|
|
matchesWorkerWorkOrderKeyword,
|
|
patchWorkerWorkOrderInfiniteEnvelope,
|
|
patchWorkerWorkOrderListEnvelope,
|
|
resolveWorkerMyOrderRealtimeVerdict,
|
|
} from '../src/utils/worker-realtime-work-orders.ts'
|
|
|
|
function buildRow(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
workOrderId: 9001,
|
|
workOrderNo: 'WO26093017345168',
|
|
status: 'in_progress',
|
|
productName: '某游戏代练',
|
|
platformOrderId: '2609301734',
|
|
material: { collect: { fields: { gameId: 'g-1', gameNickname: '小明' } } },
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function buildEnvelope(items: unknown[], total = items.length) {
|
|
return { ok: true, data: { items, pagination: { page: 1, pageSize: 20, total } } }
|
|
}
|
|
|
|
function itemsOf(envelope: unknown): Array<{ workOrderId: number }> {
|
|
return (envelope as { data: { items: Array<{ workOrderId: number }> } }).data.items
|
|
}
|
|
|
|
function totalOf(envelope: unknown): number {
|
|
return (envelope as { data: { pagination: { total: number } } }).data.pagination.total
|
|
}
|
|
|
|
test('按工单号搜索必须能命中(后端搜索串第一项就是工单号,前端曾完全没比对它)', () => {
|
|
assert.equal(matchesWorkerWorkOrderKeyword(buildRow(), 'WO26093017345168'), true)
|
|
assert.equal(matchesWorkerWorkOrderKeyword(buildRow(), 'wo26093017'), true)
|
|
// 原有字段仍然要能命中
|
|
assert.equal(matchesWorkerWorkOrderKeyword(buildRow(), '小明'), true)
|
|
assert.equal(matchesWorkerWorkOrderKeyword(buildRow(), '2609301734'), true)
|
|
})
|
|
|
|
test('关键词维度:找不到只能是 unknown,绝不能当成"不匹配"(这才是行被删空的根因)', () => {
|
|
const row = buildRow()
|
|
assert.equal(evaluateWorkerKeywordVerdict(row, 'WO26093017345168'), 'match')
|
|
assert.equal(evaluateWorkerKeywordVerdict(row, ''), 'match')
|
|
// 后端还能搜 material_json 里前端不知道的字段,所以找不到 ≠ 不匹配
|
|
assert.equal(evaluateWorkerKeywordVerdict(row, '后端才知道的字段'), 'unknown')
|
|
})
|
|
|
|
test('按工单号搜到的行,实时事件来了也不能被删掉,并且总数要跟着走', () => {
|
|
const row = buildRow({ status: 'pending_material' })
|
|
const envelope = buildEnvelope([row])
|
|
const snapshot = buildRow({ status: 'in_progress' })
|
|
|
|
// 关键词命中 → upsert,原地更新,行数不变
|
|
const updated = patchWorkerWorkOrderListEnvelope(
|
|
envelope,
|
|
{ operation: 'upsert', data: snapshot },
|
|
false,
|
|
)
|
|
assert.equal(itemsOf(updated).length, 1)
|
|
assert.equal(totalOf(updated), 1)
|
|
assert.equal((itemsOf(updated)[0] as { status?: string }).status, 'in_progress')
|
|
})
|
|
|
|
test('大厅轻量快照就地更新时保留 myShare 等个性化字段', () => {
|
|
const envelope = buildEnvelope([
|
|
buildRow({ status: 'open', myShare: { quantity: 2 }, categoryName: '密钥' }),
|
|
])
|
|
const updated = patchWorkerWorkOrderListEnvelope(
|
|
envelope,
|
|
{
|
|
operation: 'upsert',
|
|
data: { workOrderId: 9001, status: 'open', productName: '新商品名' },
|
|
},
|
|
false,
|
|
)
|
|
const item = itemsOf(updated)[0] as {
|
|
productName?: string
|
|
myShare?: { quantity?: number }
|
|
categoryName?: string
|
|
}
|
|
assert.equal(item.productName, '新商品名')
|
|
assert.equal(item.myShare?.quantity, 2, '广播不带 myShare 时不能把已拼份额覆盖掉')
|
|
assert.equal(item.categoryName, '密钥')
|
|
})
|
|
|
|
test('不可证真的关键词下,快照只能就地更新:不插入、不删除', () => {
|
|
const envelope = buildEnvelope([buildRow()])
|
|
// 关键词没命中前端已知字段(unknown):既不该把它当"不匹配"删掉
|
|
const patched = patchWorkerWorkOrderListEnvelope(
|
|
envelope,
|
|
{ operation: 'upsert', data: buildRow({ status: 'accepted' }) },
|
|
false,
|
|
)
|
|
assert.equal(itemsOf(patched).length, 1)
|
|
// 也不该把不在列表里的行插进来(留着让后端说了算)
|
|
const notInserted = patchWorkerWorkOrderListEnvelope(
|
|
envelope,
|
|
{ operation: 'upsert', data: buildRow({ workOrderId: 8888 }) },
|
|
false,
|
|
)
|
|
assert.equal(itemsOf(notInserted).length, 1)
|
|
})
|
|
|
|
test('真正移出可见范围时要删行,并同步分页总数(旧实现只删 items,分页器还显示有 N 条)', () => {
|
|
const envelope = buildEnvelope([buildRow(), buildRow({ workOrderId: 9002 })], 2)
|
|
const removed = patchWorkerWorkOrderListEnvelope(
|
|
envelope,
|
|
{ operation: 'remove', data: buildRow() },
|
|
false,
|
|
)
|
|
assert.deepEqual(
|
|
itemsOf(removed).map((item) => item.workOrderId),
|
|
[9002],
|
|
)
|
|
assert.equal(totalOf(removed), 1, '删行后总数必须减一')
|
|
|
|
const inserted = patchWorkerWorkOrderListEnvelope(
|
|
buildEnvelope([buildRow()], 1),
|
|
{ operation: 'upsert', data: buildRow({ workOrderId: 9003 }) },
|
|
true,
|
|
)
|
|
assert.equal(itemsOf(inserted).length, 2)
|
|
assert.equal(totalOf(inserted), 2, '插行后总数必须加一')
|
|
})
|
|
|
|
test('无限下拉:只有第 1 页允许插入,其余页只就地更新', () => {
|
|
const page = (ids: number[]) =>
|
|
buildEnvelope(ids.map((id) => buildRow({ workOrderId: id, status: 'open' })))
|
|
const current = { pages: [page([1, 2]), page([3, 4])], pageParams: [1, 2] }
|
|
const patched = patchWorkerWorkOrderInfiniteEnvelope(
|
|
current,
|
|
{ operation: 'upsert', data: buildRow({ workOrderId: 99, status: 'open' }) },
|
|
true,
|
|
) as { pages: unknown[] }
|
|
assert.equal(itemsOf(patched.pages[0]).length, 3, '第 1 页应插入')
|
|
assert.equal(itemsOf(patched.pages[1]).length, 2, '第 2 页不得插入')
|
|
})
|
|
|
|
test('大厅新单插入后,置顶单必须仍在最前方', () => {
|
|
const current = {
|
|
pages: [
|
|
buildEnvelope([
|
|
buildRow({ workOrderId: 100, status: 'open', pinnedAt: '2026-10-25T10:00:00+08:00' }),
|
|
buildRow({ workOrderId: 200, status: 'open' }),
|
|
]),
|
|
],
|
|
pageParams: [1],
|
|
}
|
|
const patched = patchWorkerWorkOrderInfiniteEnvelope(
|
|
current,
|
|
{ operation: 'upsert', data: buildRow({ workOrderId: 300, status: 'open' }) },
|
|
true,
|
|
) as { pages: unknown[] }
|
|
assert.deepEqual(
|
|
itemsOf(patched.pages[0]).map((item) => item.workOrderId),
|
|
[100, 300, 200],
|
|
'新单必须排在置顶单之后,未置顶部分仍按 id 倒序',
|
|
)
|
|
})
|
|
|
|
test('多个置顶单之间按置顶时间倒序,且都排在普通单之前', () => {
|
|
const current = {
|
|
pages: [
|
|
buildEnvelope([
|
|
buildRow({ workOrderId: 100, status: 'open', pinnedAt: '2026-10-25T09:00:00+08:00' }),
|
|
buildRow({ workOrderId: 200, status: 'open' }),
|
|
buildRow({ workOrderId: 300, status: 'open', pinnedAt: '2026-10-25T11:00:00+08:00' }),
|
|
]),
|
|
],
|
|
pageParams: [1],
|
|
}
|
|
const patched = patchWorkerWorkOrderInfiniteEnvelope(
|
|
current,
|
|
{ operation: 'upsert', data: buildRow({ workOrderId: 400, status: 'open' }) },
|
|
true,
|
|
) as { pages: unknown[] }
|
|
assert.deepEqual(
|
|
itemsOf(patched.pages[0]).map((item) => item.workOrderId),
|
|
[300, 100, 400, 200],
|
|
)
|
|
})
|
|
|
|
test('第 2 页的工单被置顶后必须提到第 1 页最前', () => {
|
|
const current = {
|
|
pages: [
|
|
buildEnvelope([
|
|
buildRow({ workOrderId: 300, status: 'open' }),
|
|
buildRow({ workOrderId: 299, status: 'open' }),
|
|
]),
|
|
buildEnvelope([buildRow({ workOrderId: 100, status: 'open' })]),
|
|
],
|
|
pageParams: [1, 2],
|
|
}
|
|
const patched = patchWorkerWorkOrderInfiniteEnvelope(
|
|
current,
|
|
{
|
|
operation: 'upsert',
|
|
data: buildRow({
|
|
workOrderId: 100,
|
|
status: 'open',
|
|
pinnedAt: '2026-10-25T12:00:00+08:00',
|
|
}),
|
|
},
|
|
false,
|
|
) as { pages: unknown[] }
|
|
assert.deepEqual(
|
|
itemsOf(patched.pages[0]).map((item) => item.workOrderId),
|
|
[100, 300],
|
|
'置顶单要在第 1 页最前,被顶下去的普通单顺延到第 2 页',
|
|
)
|
|
assert.deepEqual(
|
|
itemsOf(patched.pages[1]).map((item) => item.workOrderId),
|
|
[299],
|
|
)
|
|
})
|
|
|
|
/**
|
|
* 「我的订单」状态页签的实时删行判定。
|
|
*
|
|
* 生产现象(与后台同源):按工单号搜出来的那一单,状态一流转(代练中 → 待验收 / 已验收)
|
|
* 就被实时补丁按"状态不匹配"删掉,列表变空、必须刷新整页。
|
|
* 页签只是展示偏好:后端按单号查时已经忽略状态,前端不能再拿状态删行。
|
|
*/
|
|
test('显式单号检索:状态页签不匹配也不得删行(状态流转后仍要能搜到)', () => {
|
|
const row = buildRow({ status: 'in_progress' })
|
|
const verdict = resolveWorkerMyOrderRealtimeVerdict({
|
|
item: buildRow({ status: 'pending_acceptance' }),
|
|
statusFilter: 'in_progress',
|
|
keyword: 'WO26093017345168',
|
|
operation: 'upsert',
|
|
page: 1,
|
|
explicitOrderNo: true,
|
|
})
|
|
assert.equal(verdict.operation, 'upsert', '单号检索下状态不能证伪')
|
|
assert.equal(verdict.shouldInsert, true, '命中的单号仍可插入第 1 页')
|
|
|
|
const updated = patchWorkerWorkOrderListEnvelope(
|
|
buildEnvelope([row]),
|
|
{ operation: verdict.operation, data: buildRow({ status: 'pending_acceptance' }) },
|
|
verdict.shouldInsert,
|
|
)
|
|
assert.equal(itemsOf(updated).length, 1, '行不能被删掉')
|
|
assert.equal(
|
|
(itemsOf(updated)[0] as { status?: string }).status,
|
|
'pending_acceptance',
|
|
'应就地更新成最新状态',
|
|
)
|
|
})
|
|
|
|
test('模糊关键词:状态页签不匹配仍按原设计移除行', () => {
|
|
const verdict = resolveWorkerMyOrderRealtimeVerdict({
|
|
item: buildRow({ status: 'accepted' }),
|
|
statusFilter: 'in_progress',
|
|
keyword: '暗潮',
|
|
operation: 'upsert',
|
|
page: 1,
|
|
explicitOrderNo: false,
|
|
})
|
|
assert.equal(verdict.operation, 'remove')
|
|
assert.equal(verdict.shouldInsert, false)
|
|
})
|
|
|
|
test('服务端 remove(打手已失去该单)仍然生效,单号护栏不误挡', () => {
|
|
const verdict = resolveWorkerMyOrderRealtimeVerdict({
|
|
item: buildRow({ status: 'in_progress' }),
|
|
statusFilter: '',
|
|
keyword: 'WO26093017345168',
|
|
operation: 'remove',
|
|
page: 1,
|
|
explicitOrderNo: true,
|
|
})
|
|
assert.equal(verdict.operation, 'remove', '服务端明确说单子已不可见时必须移除')
|
|
})
|
|
|
|
test('非第 1 页不得插入,只做就地更新', () => {
|
|
const verdict = resolveWorkerMyOrderRealtimeVerdict({
|
|
item: buildRow({ workOrderId: 8888 }),
|
|
statusFilter: '',
|
|
keyword: 'WO26093017345168',
|
|
operation: 'upsert',
|
|
page: 2,
|
|
explicitOrderNo: true,
|
|
})
|
|
assert.equal(verdict.operation, 'upsert')
|
|
assert.equal(verdict.shouldInsert, false)
|
|
})
|