diff --git a/frontend/index.html b/frontend/index.html index b5c2c1f..60a82bc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1559,7 +1559,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.5.5'; +const SIMPLE_VERSION = '3.5.6'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -7710,19 +7710,26 @@ function stkMenuNaiSuffix(item) { return (item && item.nai_name) ? '\u25b8 ' + item.nai_name : ''; } +// The overlay's item list, shared by the cached top menu and the pending +// SELECT ITEM items the card returns mid-dialogue: one row per item, each +// with the card-provided next action suffix. `handler` is the click callback +// (stkMenuItemClick / stkSubItemClick). +function stkMenuItemsHtml(items, handler) { + let html = '
'; + (items || []).forEach(item => { + const nai = stkMenuNaiSuffix(item); + html += '
' + item.id + '' + esc(item.text) + '' + + (nai ? '' + esc(nai) + '' : '') + '
'; + }); + return html + '
'; +} + function stkMenuRenderItems() { const content = document.getElementById('stk-menu-panel-content'); content.innerHTML = 'Loading...'; pysimFetch('/api/menu').then(data => { if (!data || !data.items) { content.innerHTML = 'No menu data'; return; } - let html = '
'; - data.items.forEach(item => { - const nai = stkMenuNaiSuffix(item); - html += '
' + item.id + '' + esc(item.text) + '' + - (nai ? '' + esc(nai) + '' : '') + '
'; - }); - html += '
'; - content.innerHTML = html; + content.innerHTML = stkMenuItemsHtml(data.items, 'stkMenuItemClick'); }).catch(() => { content.innerHTML = 'Error loading menu'; }); } @@ -7751,14 +7758,7 @@ function stkMenuHandleResponse(data) { btns.classList.remove('hidden'); } else if (data.type === 'select_item') { stkMenuStack.push(data.items); - content.innerHTML = ''; - const items = data.items; - let html = '
'; - items.forEach(item => { - html += '
' + item.id + '' + esc(item.text) + '
'; - }); - html += '
'; - content.innerHTML = html; + content.innerHTML = stkMenuItemsHtml(data.items, 'stkSubItemClick'); backBtn.style.display = ''; document.getElementById('stk-ok-btn').style.display = 'none'; btns.classList.remove('hidden'); diff --git a/frontend/sw.js b/frontend/sw.js index 21b8e92..65aa7fb 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v258'; +const CACHE = 'simple-v259'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/stk_menu.test.js b/frontend/tests/stk_menu.test.js index 709db1a..dcb7494 100644 --- a/frontend/tests/stk_menu.test.js +++ b/frontend/tests/stk_menu.test.js @@ -23,6 +23,7 @@ function extractFunc(src, name, asyncFn) { let code = extractFunc(html, 'stkMenuRespond', true) + '\n'; code += extractFunc(html, 'stkMenuNaiSuffix') + '\n'; +code += extractFunc(html, 'stkMenuItemsHtml') + '\n'; code += 'globalThis.esc = s => s;\n'; eval(code); @@ -47,6 +48,27 @@ test('the STK menu item suffix shows the item next action (8.24)', () => { assert.strictEqual(stkMenuNaiSuffix(null), ''); }); +test('the overlay item list shows the next action and the row handler', () => { + const list = stkMenuItemsHtml([ + { id: 1, text: 'Menu', nai: 0x25, nai_name: 'SET UP MENU' }, + { id: 2, text: 'Info' }, + ], 'stkSubItemClick'); + assert.match(list, /onclick="stkSubItemClick\(1\)"/); + assert.match(list, /onclick="stkSubItemClick\(2\)"/); + assert.ok(list.includes('Menu')); + assert.ok(list.includes('\u25b8 SET UP MENU')); + // the second item has no indicator -> exactly one suffix in the list + assert.strictEqual((list.match(/\u25b8/g) || []).length, 1); + assert.strictEqual(stkMenuItemsHtml(null, 'stkMenuItemClick'), '
'); +}); + +test('both overlay lists render through the shared row helper', () => { + // the cached top menu and the pending SELECT ITEM items must use the same + // renderer, otherwise the latter silently loses the NAI suffix + assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkMenuItemClick'\)/); + assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkSubItemClick'\)/); +}); + test('back with a fetched SELECT ITEM continues the card dialogue', async () => { const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] }; const calls = setup(data); diff --git a/pyproject.toml b/pyproject.toml index 231e854..1314574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.5.5" +version = "3.5.6" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index a2eb4d3..8266d39 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.5.5' +VERSION = '3.5.6' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE