const { test } = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); function extractFunc(src, name) { const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); const m = re.exec(src); if (!m) throw new Error('function ' + name + ' not found'); let i = m.index + m[0].length - 1; let depth = 0; for (; i < src.length; i++) { if (src[i] === '{') depth++; else if (src[i] === '}') { depth--; if (depth === 0) break; } } return src.slice(m.index, i + 1); } // Extract chain builder functions and dependencies const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText', 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; let code = ''; for (const f of FNS) { code += extractFunc(html, f) + '\n'; } const m = html.match(/const _chains = \{\};/); if (m) code += m[0].replace(/^const /, 'var ') + '\n'; const swm = html.match(/const SW_MAP = \{[\s\S]*?\n\};/); if (swm) code += swm[0].replace(/^const /, 'var ') + '\n'; const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/); if (pn) code += pn[0].replace(/^const /, 'var ') + '\n'; const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/); if (an) code += an[0].replace(/^const /, 'var ') + '\n'; const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/); if (ar) code += ar[0].replace(/^const /, 'var ') + '\n'; eval(code); code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n'; eval(code); const els = {}; const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } }; global.document = doc; // ramApplyCard/ramExecute delegate to the SP-form helpers; their real // behaviour (and the preset re-read before an operation) is covered by // cards_counter.test.js, so keep them as global stubs here. globalThis.cardsApply = () => {}; globalThis.spRefreshFromPreset = () => ''; function reset() { for (const id of Object.keys(els)) delete els[id]; } function genRamResult(fields) { const row = { cmd: fields.cmd, fields: fields }; return chainRamBuildRowHex(0, row); } function genRamApdu() { return genRamResult({ cmd: 'install-install', aid: 'A000000151000000', elfAid: '', modAid: '', priv: '00', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0', }); } test('UICC toolkit nested inside EA (m=2, services 0)', () => { const apdu = genRamApdu(); assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0100'), apdu); }); test('UICC m=1 emits single pair', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0', }); assert.ok(apdu.includes('EA11800F0000000101010002011603AF4D0100'), apdu); }); test('UICC m=3 fills middle pair with 0000', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0', }); assert.ok(apdu.includes('EA158013000000030101000003030002011603AF4D0100'), apdu); }); test('UICC services 7 appended as final byte', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '7', }); assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0107'), apdu); }); test('SIM (CA) access domain FIRST, no services byte', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '5A', tkServices: '0', }); assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603AF4D01'), apdu); }); test('SIM (CA) blank access domain emits length byte 00', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0', }); assert.ok(apdu.includes('EF13CA110000000002010102020002011603AF4D01'), apdu); }); test('SIM (CA) m=3, no TAR, blank access domain', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03', tkChannels: '0', tkMsl: '16', tkTar: '', tkAd: '', tkServices: '0', }); assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu); }); test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => { const apdu = genRamResult({ cmd: 'install-install', aid: 'A000000151000000', priv: '00', tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0', tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C', tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0', }); assert.ok(apdu.includes('EA8188808185'), apdu); }); test('LOAD P1 fixed to 80 (last block)', () => { const apdu = genRamResult({ cmd: 'load', data: 'AABBCC', block: '0' }); assert.ok(apdu.startsWith('80E88000'), apdu); }); test('DELETE: P1=00, mode in P2', () => { let apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '00' }); assert.ok(apdu.startsWith('80E40000'), apdu); apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '80' }); assert.ok(apdu.startsWith('80E40080'), apdu); }); test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => { for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) { const apdu = genRamResult({ cmd: 'store-data', data: 'AABB', enc: enc, block: '0' }); assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1); } }); test('ramRenderExploreHtml puts the command format after the resource lines', () => { global.t = s => s; const out = ramRenderExploreHtml({ appCount: 5, freeNV: 100, freeV: 50 }, [], [], [], 'compact'); delete global.t; const iNv = out.indexOf('Free NV:'); const iV = out.indexOf('Free Volatile:'); const iFmt = out.indexOf('RAM command format:'); assert.ok(iNv >= 0 && iV >= 0 && iFmt > iV, out); }); test('explorer action buttons share a fixed-width column, Install is green', () => { global.t = s => s; const out = ramRenderExploreHtml( { appCount: 1, freeNV: 10, freeV: 5 }, [], [{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }], [{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'] }] ); delete global.t; // every action cell is the same fixed-width column assert.ok(out.includes('w-28 shrink-0'), out); // rows are visible chips (background + hover) so the button belongs to its row assert.ok(out.includes('bg-gray-50 dark:bg-slate-800/60'), out); assert.ok(out.includes('hover:bg-gray-100 dark:hover:bg-slate-700/50'), out); // the Install button (module without an instance) is green, not blue const inst = /