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, / ({ 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/) })