Files
order_site/apps/frontend/tests/worker-realtime-work-orders.test.ts
T

329 lines
12 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('不可证真的关键词下,快照只能就地更新:不插入、不删除', () => {
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 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)
})
/**
* 同一打手在同一个工单多次拼单:一张单可以同时属于「代练中」和「待验收」,
* 但快照的 status 只能是一个值。前端若按单个 status 删行,另一个页签就会莫名其妙变空
* (线上「待验收有时候没有数据」)。服务端现在带 workerStatuses,按集合判归属。
*/
test('多份额工单:快照 status 是进行中,但属于待验收时不得从待验收页签删行', () => {
const snapshot = buildRow({
status: 'in_progress',
workerStatuses: ['in_progress', 'pending_acceptance'],
})
const verdict = resolveWorkerMyOrderRealtimeVerdict({
item: snapshot,
statusFilter: 'pending_acceptance',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(verdict.operation, 'upsert', '待验收页签必须保留这张多份额工单')
assert.equal(verdict.shouldInsert, true)
const updated = patchWorkerWorkOrderListEnvelope(
buildEnvelope([buildRow({ status: 'pending_acceptance' })]),
{ operation: verdict.operation, data: snapshot },
verdict.shouldInsert,
)
assert.equal(itemsOf(updated).length, 1)
assert.equal((itemsOf(updated)[0] as { status?: string }).status, 'in_progress')
})
test('多份额工单:workerStatuses 同时也是代练中页签的归属依据', () => {
const verdict = resolveWorkerMyOrderRealtimeVerdict({
item: buildRow({
status: 'in_progress',
workerStatuses: ['in_progress', 'pending_acceptance'],
}),
statusFilter: 'in_progress',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(verdict.operation, 'upsert')
})
test('不在页签集合里时才允许删行(例如已验收后离开待验收页签)', () => {
const verdict = resolveWorkerMyOrderRealtimeVerdict({
item: buildRow({ status: 'accepted', workerStatuses: ['accepted'] }),
statusFilter: 'pending_acceptance',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(verdict.operation, 'remove')
assert.equal(verdict.shouldInsert, false)
})
test('VIP 待补图页签也靠 workerStatuses 判定,不会被当成状态不符删掉', () => {
const verdict = resolveWorkerMyOrderRealtimeVerdict({
item: buildRow({ status: 'accepted', workerStatuses: ['accepted', 'vip_evidence_pending'] }),
statusFilter: 'vip_evidence_pending',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(verdict.operation, 'upsert')
})
test('没有 workerStatuses 的旧快照仍按单个 status 比对(保持既有行为)', () => {
const mismatch = resolveWorkerMyOrderRealtimeVerdict({
item: buildRow({ status: 'accepted' }),
statusFilter: 'pending_acceptance',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(mismatch.operation, 'remove')
const match = resolveWorkerMyOrderRealtimeVerdict({
item: buildRow({ status: 'pending_acceptance' }),
statusFilter: 'pending_acceptance',
keyword: '',
operation: 'upsert',
page: 1,
explicitOrderNo: false,
})
assert.equal(match.operation, 'upsert')
})
/**
* 大厅按钮需求:拼过一次后按钮固定显示「再拼一次」,不再随次数变化。
* 这依赖 myShare/myShares 一直留在行上;而大厅广播是「所有打手通用」的快照,
* 不含个人份额,覆盖行时必须保留,否则按钮会被冲回「拼单 ¥x/份」。
*/
test('大厅广播覆盖行时必须保留本打手的 myShare/myShares,并更新拼单进度', () => {
const cachedRow = buildRow({
status: 'open',
myShare: { shareId: 77, quantity: 1, status: 'joined' },
myShares: [{ shareId: 77, quantity: 1, status: 'joined' }],
sharingProgress: { joinedQuantity: 1, pendingSubmissionCount: 1 },
})
const envelope = { pages: [buildEnvelope([cachedRow])], pageParams: [1] }
const snapshot = buildRow({
status: 'open',
sharingProgress: { joinedQuantity: 2, pendingSubmissionCount: 2 },
})
const patched = patchWorkerWorkOrderInfiniteEnvelope(
envelope,
{ operation: 'upsert', data: snapshot },
true,
['myShare', 'myShares'],
) as { pages: unknown[] }
const row = itemsOf(patched.pages[0])[0] as {
myShare?: { shareId: number }
myShares?: unknown[]
sharingProgress?: { joinedQuantity: number }
}
assert.equal(row.myShare?.shareId, 77, '不能把「再拼一次」的依据冲掉')
assert.equal(row.myShares?.length, 1)
assert.equal(row.sharingProgress?.joinedQuantity, 2, '公开进度仍要用广播里的最新值')
// 新插入的行本来就不属于我,不能凭空补上旧字段
const inserted = patchWorkerWorkOrderInfiniteEnvelope(
envelope,
{ operation: 'upsert', data: buildRow({ workOrderId: 9999, status: 'open' }) },
true,
['myShare', 'myShares'],
) as { pages: unknown[] }
const insertedRow = itemsOf(inserted.pages[0]).find((item) => item.workOrderId === 9999) as
| { myShare?: unknown; myShares?: unknown }
| undefined
assert.equal(insertedRow?.myShare ?? null, null)
assert.equal(insertedRow?.myShares ?? null, null)
})