GuDingZiChan/frontend/scripts/test-audit-routing.mjs

191 lines
11 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import ts from 'typescript'
import { createRenderer, h, onMounted, ref, nextTick } from 'vue'
import { createRouter, createMemoryHistory, RouterView, useRoute } from 'vue-router'
import { clearSessionState } from '../src/auth/sessionState.ts'
import { createRequestScope } from '../src/utils/requestScope.ts'
import { parseEquipmentImportRows } from '../src/utils/equipmentImport.ts'
import * as XLSX from 'xlsx'
test('the real route guard reaches login once after an expired session, without dashboard bounce', async () => {
const source = fs.readFileSync(new URL('../src/router/index.ts', import.meta.url), 'utf8')
const guardSource = source.slice(source.indexOf('router.beforeEach(async ') + 18, source.lastIndexOf('\n})') + 2)
const executable = ts.transpileModule(`const guard = ${guardSource}`.replace('import.meta.env.VITE_AUTH_MODE', "'local'"), { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText
const state = {
user: { id: 4 }, token: 'session', isAuthenticated: true,
refreshUser: async () => { throw new Error('401 session expired') },
clearSession: () => clearSessionState(state, { removeItem: () => {} })
}
const guard = new Function('useAuthStore', 'resolveAuthMode', 'keycloak', `${executable}; return guard;`)(() => state, () => 'local', {})
let nextTarget
await guard({ path: '/equipment', meta: { requiresAuth: true } }, {}, (target) => { nextTarget = target })
assert.equal(nextTarget, '/login')
nextTarget = 'not-called'
await guard({ path: '/login', meta: { requiresAuth: false } }, {}, (target) => { nextTarget = target })
assert.equal(nextTarget, undefined)
assert.equal(state.isAuthenticated, false)
})
test('changing detail IDs remounts the detail view with the key used by MainLayout', async () => {
const layout = fs.readFileSync(new URL('../src/layouts/MainLayout.vue', import.meta.url), 'utf8')
assert.match(layout, /<router-view\s+:key="route\.path"/)
const renderer = createRenderer({
createElement: (type) => ({ type, children: [] }), createText: (text) => ({ text }), createComment: (text) => ({ text }),
setText: (node, text) => { node.text = text }, setElementText: (node, text) => { node.text = text },
parentNode: (node) => node.parent || null, nextSibling: () => null, patchProp: () => {},
insert: (node, parent) => { parent.children.push(node); node.parent = parent },
remove: (node) => { if (node.parent) node.parent.children = node.parent.children.filter((item) => item !== node) }
})
let currentDetail
let mountedCount = 0
const Detail = { setup() {
const route = useRoute()
const displayedId = ref(null)
onMounted(() => { mountedCount += 1; displayedId.value = Number(route.params.id) })
currentDetail = { displayedId, targetId: () => Number(route.params.id) }
return () => h('div', String(displayedId.value))
} }
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/equipment/edit/:id', component: Detail }] })
await router.push('/equipment/edit/1')
const app = renderer.createApp({ setup() { const route = useRoute(); return () => h(RouterView, { key: route.path }) } })
app.use(router)
app.mount({ children: [] })
await nextTick()
await router.push('/equipment/edit/2')
await nextTick()
assert.equal(currentDetail.displayedId.value, 2)
assert.equal(currentDetail.targetId(), 2)
assert.equal(mountedCount, 2)
app.unmount()
})
test('print resolution requires a checked outcome and submits the originally inspected claim token', async () => {
const source = fs.readFileSync(new URL('../src/views/settings/components/PrintJobManagement.vue', import.meta.url), 'utf8')
const start = source.indexOf('const submitResolution = async () =>')
const end = source.indexOf('\nonMounted(', start)
const compiled = ts.transpileModule(source.slice(start, end), { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText
const canResolve = { value: true }
const resolutionJob = { value: { id: 83, claim_token: 'inspected-claim' } }
const resolutionOutcome = { value: 'not_printed' }
const resolutionVerified = { value: false }
const busyJobId = { value: null }
const submissions = []
let refreshes = 0
const submit = new Function('canResolve', 'resolutionJob', 'resolutionOutcome', 'resolutionVerified', 'busyJobId', 'resolvePrintJob', 'fetchJobs', 'alert', `${compiled}; return submitResolution;`)(
canResolve, resolutionJob, resolutionOutcome, resolutionVerified, busyJobId,
async (id, payload) => { submissions.push({ id, payload }) }, async () => { refreshes += 1 }, () => {}
)
await submit()
assert.equal(submissions.length, 0)
resolutionVerified.value = true
canResolve.value = false
await submit()
assert.equal(submissions.length, 0)
canResolve.value = true
await submit()
assert.deepEqual(submissions, [{ id: 83, payload: { confirmed: true, outcome: 'not_printed', expected_claim_token: 'inspected-claim' } }])
assert.equal(resolutionJob.value, null)
assert.equal(busyJobId.value, null)
assert.equal(refreshes, 1)
})
const extractListFunction = (startMarker, endMarker, bindings) => {
const source = fs.readFileSync(new URL('../src/views/equipment/EquipmentList.vue', import.meta.url), 'utf8')
const start = source.indexOf(`const ${startMarker}`)
const end = source.indexOf(`const ${endMarker}`, start)
const compiled = ts.transpileModule(source.slice(start, end), { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText
return new Function(...Object.keys(bindings), `${compiled}; return ${startMarker};`)(...Object.values(bindings))
}
for (const delayedStage of ['detail', 'records', 'fallback']) {
test(`opening asset B supersedes a slow asset A return lookup at ${delayedStage}`, async () => {
const returnDialogScope = createRequestScope()
const selectedIds = { value: [1] }
const returnAssetInfo = { value: null }
const returnMode = { value: 'batch' }
const returnAnomalyInfo = { value: null }
const returnSelectedRecordId = { value: null }
const returnRecordKeyword = { value: '' }
const showBatchReturnModal = { value: false }
let finishA
let notifyReached
const reached = new Promise((resolve) => { notifyReached = resolve })
const delayed = () => { notifyReached(); return new Promise((resolve) => { finishA = resolve }) }
const detail = (id) => ({ data: { id, equipment_code: id === 1 ? 'A' : 'B', equipment_name: id === 1 ? 'Asset A' : 'Asset B' } })
const records = (code) => ({ data: { can_return: true, equipment_code: code, equipment_name: code, borrow_records: [{ id: code === 'A' ? 101 : 202 }] } })
const bindings = {
returnDialogScope, selectedIds, returnAssetInfo, returnMode, returnAnomalyInfo,
returnSelectedRecordId, returnRecordKeyword, showBatchReturnModal,
returnSubmitting: { value: false }, borrowSubmitting: { value: false },
clearSingleActionContext: () => {},
resetReturnForm: () => { returnDialogScope.begin(); returnAssetInfo.value = null; returnSelectedRecordId.value = null },
canQuickReturnInline: () => false,
buildManualReturnRecordDisplay: (record) => String(record.id),
alert: (message) => { throw new Error(`unexpected alert: ${message}`) },
getEquipmentDetail: async (id) => id === 1 && delayedStage === 'detail' ? delayed() : detail(id),
getBorrowRecordByCode: async (code) => {
if (code === 'A' && delayedStage === 'records') return delayed()
if (code === 'A' && delayedStage === 'fallback') return { data: { ...records('A').data, can_return: false } }
return records(code)
},
searchReturnAssets: async () => delayed()
}
const open = extractListFunction('openBatchReturnModalInternal', 'openBatchReturnModal =', bindings)
const pendingA = open()
await reached
selectedIds.value = [2]
await open()
assert.equal(returnAssetInfo.value.equipment_code, 'B')
assert.equal(returnSelectedRecordId.value, 202)
finishA(delayedStage === 'detail' ? detail(1) : delayedStage === 'records' ? records('A') : { data: [{ equipment_id: 1, can_return: false }] })
await pendingA
assert.equal(returnAssetInfo.value.equipment_code, 'B')
assert.equal(returnSelectedRecordId.value, 202)
assert.equal(returnMode.value, 'record-select')
assert.equal(returnAnomalyInfo.value, null)
})
}
test('successful single action preserves prior cross-page selections and removes only its successful asset', () => {
const selectedIds = { value: [30] }
const singleActionContext = { value: { previousSelectedIds: [1, 21, 30] } }
const selectionEquipment = { value: new Map([[1, { equipment_code: 'A' }], [21, { equipment_code: 'B' }], [30, { equipment_code: 'Z' }]]) }
const finish = extractListFunction('finishSuccessfulAssetAction', 'restoreSelectionAfterSingleActionCancel', {
selectedIds, singleActionContext, selectionEquipment,
clearSingleActionContext: () => { singleActionContext.value = null }
})
finish(new Set(['Z']))
assert.deepEqual(selectedIds.value, [1, 21])
assert.equal(singleActionContext.value, null)
})
test('a workbook starting at A3 reports its data row as actual Excel row 4', () => {
const sheet = {
A3: { t: 's', v: 'name' }, B3: { t: 's', v: 'type' },
A4: { t: 's', v: 'asset' }, B4: { t: 's', v: 'computer' }, G4: { t: 'n', v: 1 },
'!ref': 'A3:G4'
}
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, sheet, 'assets')
const reread = XLSX.read(XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }), { type: 'buffer' })
const actual = reread.Sheets.assets
const rows = XLSX.utils.sheet_to_json(actual, { header: 1, blankrows: true })
assert.equal(parseEquipmentImportRows(rows, XLSX.utils.decode_range(actual['!ref']).s.r)[0].source_row, 4)
})
test('sending more than 100 remote labels refuses before generating or creating any real task', async () => {
const source = fs.readFileSync(new URL('../src/components/BarcodePrint.vue', import.meta.url), 'utf8')
const start = source.indexOf('const handleRemotePrint = async () =>')
const end = source.indexOf('const handleCancelRemoteJob', start)
const compiled = ts.transpileModule(source.slice(start, end), { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText
const notices = []
const remotePrint = new Function('props', 'equipmentList', 'alert', `${compiled}; return handleRemotePrint;`)(
{ equipmentIds: Array.from({ length: 101 }, (_, index) => index + 1) }, { value: [] }, (message) => { notices.push(message) }
)
await remotePrint()
assert.equal(notices.length, 1)
assert.match(notices[0], /100/)
})