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