diff --git a/frontend/index.html b/frontend/index.html index 0c28a9b..c618e37 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4806,10 +4806,16 @@ async function pysimSendOta() { // reuse /api/send-ota. Install Package sends the .cap hex to /api/ram-install // which orchestrates INSTALL[for load] -> LOAD x N -> INSTALL[for install]. +let _ramCardIdx = null; +let _ramOpLast = null; + function ramRender() { - // populate the card preset selector from the in-memory cards[] array + // populate the card preset selector from the in-memory cards[] array, + // keeping the current/remembered selection across rebuilds const sel = document.getElementById('ram-card-sel'); if (!sel) return; + const prev = parseInt(sel.value, 10); + const keep = (!isNaN(prev) && cards[prev]) ? prev : _ramCardIdx; sel.innerHTML = ''; cards.forEach((c, i) => { const opt = document.createElement('option'); @@ -4817,23 +4823,23 @@ function ramRender() { opt.textContent = c.name || ('Card ' + i); sel.appendChild(opt); }); + if (keep !== null && keep !== undefined && cards[keep]) sel.value = String(keep); ramOpChanged(); } function ramApplyCard(idx) { // copy the selected card preset into the SP form fields so that // getRamSpParams() picks up the right SPI/KIc/KID/TAR/CNTR/keys + const i = parseInt(idx, 10); + if (!isNaN(i) && cards[i]) _ramCardIdx = i; cardsApply(idx); } function ramOpChanged() { const op = document.getElementById('ram-op').value; + if (_ramOpLast !== null && op !== _ramOpLast) ramClearResults(); + _ramOpLast = op; document.getElementById('ram-install-params').classList.toggle('hidden', op !== 'install-cap'); - if (op !== 'explore') { - _ramExplorerData = null; - document.getElementById('ram-explorer').classList.add('hidden'); - document.getElementById('ram-explorer').innerHTML = ''; - } } function ramShowProgress(text) { @@ -4848,6 +4854,7 @@ function ramHideProgress() { function ramClearResults() { _ramExplorerData = null; + ramHideProgress(); document.getElementById('ram-result').classList.add('hidden'); document.getElementById('ram-explorer').classList.add('hidden'); document.getElementById('ram-explorer').innerHTML = ''; @@ -5384,6 +5391,8 @@ async function ramInstallCap(sp) { async function ramExecute() { ramClearResults(); + const cardIdx = parseInt(document.getElementById('ram-card-sel').value, 10); + if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx; const op = document.getElementById('ram-op').value; const sp = getRamSpParams(); if (!sp.kicKey || !sp.kidKey) { @@ -5589,7 +5598,14 @@ function cardsAdd() { }); } +function ramCardIdxAfterRemove(idx, removedIdx) { + if (idx === null || idx === undefined || idx < 0) return null; + if (idx === removedIdx) return null; + return idx > removedIdx ? idx - 1 : idx; +} + function cardsRemove(i) { + _ramCardIdx = ramCardIdxAfterRemove(_ramCardIdx, i); cards.splice(i, 1); cardsSave(); cardsRender(); diff --git a/frontend/sw.js b/frontend/sw.js index 6a8b69d..168f4c3 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v126'; +const CACHE = 'otaman-v127'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 878f789..bb8f23c 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -6,7 +6,7 @@ const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); function extractFunc(src, name) { - const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); + 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; @@ -22,7 +22,8 @@ function extractFunc(src, name) { } // Extract chain builder functions and dependencies -const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml']; +const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', + 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute']; let code = ''; for (const f of FNS) { code += extractFunc(html, f) + '\n'; @@ -32,6 +33,8 @@ if (m) code += m[0].replace(/^const /, 'var ') + '\n'; const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/); if (lc) code += lc[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]; } }; @@ -197,3 +200,123 @@ test('ramFmtPrivileges uses the translated (none) placeholder', () => { assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)'); delete global.t; }); + +function fakeClassList() { + const set = new Set(); + return { + add: (...cs) => cs.forEach(c => set.add(c)), + remove: (...cs) => cs.forEach(c => set.delete(c)), + contains: c => set.has(c), + toggle: (c, on) => { if (on === undefined ? !set.has(c) : on) set.add(c); else set.delete(c); }, + }; +} + +function fakeEl(id) { + return { + id, + value: '', + innerHTML: '', + textContent: '', + classList: fakeClassList(), + options: [], + appendChild(opt) { this.options.push(opt); }, + }; +} + +function fakeRamDocument(ids) { + const els = {}; + for (const id of ids) els[id] = fakeEl(id); + const sel = els['ram-card-sel']; + if (sel) { + Object.defineProperty(sel, 'innerHTML', { + get() { return this._html || ''; }, + set(v) { this._html = v; this.value = ''; }, + }); + } + globalThis.document = { + getElementById: id => els[id] || null, + createElement: () => fakeEl('option'), + }; + return els; +} + +test('ramOpChanged clears the executed status only on a real op change', () => { + const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']); + _ramOpLast = null; + els['ram-op'].value = 'explore'; + ramOpChanged(); + assert.ok(!els['ram-result'].classList.contains('hidden')); + els['ram-result'].classList.remove('hidden'); + els['ram-steps'].classList.remove('hidden'); + ramOpChanged(); + assert.ok(!els['ram-result'].classList.contains('hidden'), 'same op must keep the result'); + els['ram-op'].value = 'install-cap'; + ramOpChanged(); + assert.ok(els['ram-result'].classList.contains('hidden')); + assert.ok(els['ram-steps'].classList.contains('hidden')); + assert.ok(els['ram-explorer'].classList.contains('hidden')); + assert.ok(els['ram-progress'].classList.contains('hidden')); + assert.ok(!els['ram-install-params'].classList.contains('hidden')); +}); + +test('ramRender keeps the selected card preset across rebuilds', () => { + const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']); + globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }]; + _ramCardIdx = null; + _ramOpLast = 'explore'; + els['ram-op'].value = 'explore'; + ramRender(); + assert.strictEqual(els['ram-card-sel'].value, ''); + els['ram-card-sel'].value = '1'; + ramRender(); + assert.strictEqual(els['ram-card-sel'].value, '1'); + els['ram-card-sel'].value = ''; + _ramCardIdx = 2; + ramRender(); + assert.strictEqual(els['ram-card-sel'].value, '2'); + globalThis.cards = [{ name: 'A' }]; + _ramCardIdx = 2; + ramRender(); + assert.strictEqual(els['ram-card-sel'].value, ''); + delete globalThis.cards; +}); + +test('ramApplyCard remembers a valid picked preset', () => { + globalThis.cards = [{ name: 'A' }, { name: 'B' }]; + let applied = null; + globalThis.cardsApply = i => { applied = i; }; + _ramCardIdx = null; + ramApplyCard('1'); + assert.strictEqual(_ramCardIdx, 1); + assert.strictEqual(applied, '1'); + ramApplyCard(''); + assert.strictEqual(_ramCardIdx, 1, 'invalid pick must not forget the preset'); + delete globalThis.cards; + delete globalThis.cardsApply; +}); + +test('ramExecute commits the dropdown selection before running', async () => { + const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']); + globalThis.cards = [{ name: 'A' }]; + globalThis.getRamSpParams = () => ({ kicKey: '11', kidKey: '22' }); + let explored = false; + globalThis.ramExplore = async () => { explored = true; }; + globalThis.alert = () => {}; + _ramCardIdx = null; + els['ram-card-sel'].value = '0'; + els['ram-op'].value = 'explore'; + await ramExecute(); + assert.strictEqual(_ramCardIdx, 0); + assert.ok(explored); + delete globalThis.cards; + delete globalThis.getRamSpParams; + delete globalThis.ramExplore; + delete globalThis.alert; +}); + +test('ramCardIdxAfterRemove keeps the remembered index aligned', () => { + assert.strictEqual(ramCardIdxAfterRemove(2, 0), 1); + assert.strictEqual(ramCardIdxAfterRemove(0, 0), null); + assert.strictEqual(ramCardIdxAfterRemove(0, 2), 0); + assert.strictEqual(ramCardIdxAfterRemove(null, 1), null); +});