124 lines
5.9 KiB
TypeScript
124 lines
5.9 KiB
TypeScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { toApiDateTime } from '../src/utils/dateTime.ts'
|
|
import { clearSessionState } from '../src/auth/sessionState.ts'
|
|
import { createRequestScope } from '../src/utils/requestScope.ts'
|
|
import { indexEquipmentSelection, resolveSelectedEquipmentCodes } from '../src/utils/equipmentSelection.ts'
|
|
import { parseEquipmentImportRows } from '../src/utils/equipmentImport.ts'
|
|
import { getReturnCompletionText, submitReturnWithConfirmation } from '../src/utils/returnConfirmation.ts'
|
|
|
|
test('local browser datetime becomes an unambiguous instant and survives server timezone changes', () => {
|
|
const previousTimezone = process.env.TZ
|
|
try {
|
|
process.env.TZ = 'Asia/Shanghai'
|
|
const payload = toApiDateTime('2026-10-09T12:00')
|
|
assert.equal(payload, '2026-10-09T04:00:00.000Z')
|
|
process.env.TZ = 'UTC'
|
|
assert.equal(new Date(payload!).toISOString(), payload)
|
|
assert.equal(toApiDateTime(''), undefined)
|
|
assert.throws(() => toApiDateTime('not-a-date'), /无效/)
|
|
} finally {
|
|
if (previousTimezone === undefined) delete process.env.TZ
|
|
else process.env.TZ = previousTimezone
|
|
}
|
|
})
|
|
|
|
test('failed user refresh clears both live authentication and the persisted token', () => {
|
|
const removed: string[] = []
|
|
const state = { user: { id: 4 }, token: 'session' as string | null, isAuthenticated: true }
|
|
clearSessionState(state, { removeItem: (key) => { removed.push(key) } })
|
|
assert.deepEqual(state, { user: null, token: null, isAuthenticated: false })
|
|
assert.deepEqual(removed, ['token'])
|
|
})
|
|
|
|
test('late requests cannot replace a newer detail or mutate a disposed view', async () => {
|
|
const scope = createRequestScope()
|
|
let displayed = ''
|
|
let finishFirst: () => void = () => {}
|
|
const first = scope.begin()
|
|
const delayed = new Promise<void>((resolve) => { finishFirst = resolve }).then(() => {
|
|
if (scope.isCurrent(first)) displayed = 'asset A'
|
|
})
|
|
const second = scope.begin()
|
|
if (scope.isCurrent(second)) displayed = 'asset B'
|
|
finishFirst()
|
|
await delayed
|
|
assert.equal(displayed, 'asset B')
|
|
scope.dispose()
|
|
assert.equal(scope.isCurrent(second), false)
|
|
})
|
|
|
|
test('all selected asset codes survive page changes, including nested accessories', () => {
|
|
const pageOne = indexEquipmentSelection([{ id: 1, equipment_code: 'A', equipment_name: 'A', children: [
|
|
{ id: 2, equipment_code: 'A-CHILD', equipment_name: 'child', parent_id: 1 }
|
|
] }])
|
|
const pageTwo = indexEquipmentSelection([{ id: 21, equipment_code: 'B', equipment_name: 'B' }])
|
|
const selected = new Map([...pageOne, ...pageTwo])
|
|
assert.deepEqual(resolveSelectedEquipmentCodes([1, 2, 21], selected), ['A', 'A-CHILD', 'B'])
|
|
assert.throws(() => resolveSelectedEquipmentCodes([1, 999, 21], selected), /#999/)
|
|
})
|
|
|
|
test('Excel data validation preserves invalid rows and original row numbers through empty rows', () => {
|
|
const rows = parseEquipmentImportRows([
|
|
['name', 'type'],
|
|
['zero', 'computer', '', '', '', 100, 0],
|
|
[],
|
|
['missing type', '', '', '', '', 50, 2],
|
|
['bad quantity', 'computer', '', '', '', 20, 'oops'],
|
|
['negative', 'computer', '', '', '', 10, -2],
|
|
['fraction', 'computer', '', '', '', 10, 1.5]
|
|
])
|
|
assert.equal(rows.length, 5)
|
|
assert.deepEqual(rows.map((row) => row.source_row), [2, 4, 5, 6, 7])
|
|
assert.deepEqual(rows.map((row) => row.total_quantity), [0, 2, 0, -2, 1.5])
|
|
assert.equal(rows[1]?.equipment_type, '')
|
|
const shiftedHeader = parseEquipmentImportRows([['name', 'type'], ['asset', 'computer', '', '', '', 10, 1]], 2)
|
|
assert.equal(shiftedHeader[0]?.source_row, 4)
|
|
})
|
|
|
|
test('invalid nonempty Excel prices survive the real JSON transport for server rejection', () => {
|
|
const payload = JSON.parse(JSON.stringify(parseEquipmentImportRows([
|
|
['name', 'type'],
|
|
['invalid price', 'computer', '', '', '', ' bad-price ', 1],
|
|
['infinite price', 'computer', '', '', '', 'Infinity', 1],
|
|
['zero price', 'computer', '', '', '', 0, 1],
|
|
['valid price', 'computer', '', '', '', '12.50', 1],
|
|
['empty price', 'computer', '', '', '', '', 1]
|
|
])))
|
|
assert.deepEqual(payload.map((row: { unit_price?: number | string }) => row.unit_price), ['bad-price', 'Infinity', 0, 12.5, undefined])
|
|
assert.equal(payload[0].source_row, 2)
|
|
assert.equal(Number.isFinite(Number(payload[0].unit_price)), false)
|
|
assert.equal(Number.isFinite(Number(payload[1].unit_price)), false)
|
|
})
|
|
|
|
test('return confirmation acknowledges each current tree and does not report success for another need_confirm', async () => {
|
|
const tokens: Array<string | undefined> = []
|
|
let prompts = 0
|
|
const result = await submitReturnWithConfirmation({ equipment_code: 'A' }, async (payload) => {
|
|
tokens.push(payload.confirmation_token)
|
|
if (tokens.length === 1) return { data: { need_confirm: true, confirmation_token: 'tree-v1' } }
|
|
if (tokens.length === 2) return { data: { need_confirm: true, confirmation_token: 'tree-v2' } }
|
|
return { data: { need_confirm: false } }
|
|
}, () => { prompts += 1; return true })
|
|
assert.deepEqual(tokens, [undefined, 'tree-v1', 'tree-v2'])
|
|
assert.equal(prompts, 2)
|
|
assert.equal(result?.need_confirm, false)
|
|
})
|
|
|
|
test('declining differences leaves the return unsubmitted and missing confirmation tokens fail closed', async () => {
|
|
let submissions = 0
|
|
const cancelled = await submitReturnWithConfirmation({}, async () => {
|
|
submissions += 1
|
|
return { data: { need_confirm: true, confirmation_token: 'tree-v1' } }
|
|
}, () => false)
|
|
assert.equal(cancelled, null)
|
|
assert.equal(submissions, 1)
|
|
await assert.rejects(() => submitReturnWithConfirmation({}, async () => ({ data: { need_confirm: true } }), () => true), /确认信息不完整/)
|
|
})
|
|
|
|
test('lost and damaged completion messages cannot claim available stock was restored', () => {
|
|
assert.equal(getReturnCompletionText('丢失'), '已登记丢失(未入仓)')
|
|
assert.equal(getReturnCompletionText('损坏'), '已归还,状态为已损坏')
|
|
assert.equal(getReturnCompletionText('正常'), '已完成归还')
|
|
})
|