GuDingZiChan/frontend/scripts/test-audit-functional-fixes.ts

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('正常'), '已完成归还')
})