diff --git a/frontend/index.html b/frontend/index.html index 8158918..94df0b6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1903,7 +1903,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.22.5'; +const SIMPLE_VERSION = '3.22.6'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -1917,7 +1917,9 @@ let _scp80Sub = 'sp'; let _capduSub = 'sim'; let _phoneSub = 'phone'; let _scp81Sub = 'listener'; +let _pysimSub = 'files'; let _cardsView = 'list'; +let _cardsEditId = null; // the preset being edited (id; null = a new one) let _navState = null; // the state of the current history entry let _navApplying = false; // replaying: the switch functions must not record let _navOk = true; // false when pushState is unavailable (file://) @@ -1948,10 +1950,10 @@ function navCurrent() { if (_testView === 'script' && _testScriptId) st.script = _testScriptId; } } else if (_activeTab === 'scp81') st.sub = _scp81Sub; + else if (_activeTab === 'pysim') st.sub = _pysimSub; else if (_activeTab === 'cards') { st.view = _cardsView; - const p = (_cardsEditIdx !== null && cards[_cardsEditIdx]) ? cards[_cardsEditIdx] : null; - if (_cardsView === 'edit' && p) st.preset = p.id || ''; + if (_cardsView === 'edit' && _cardsEditId) st.preset = _cardsEditId; } else if (_activeTab === 'profiler') { st.view = profilerView; if (profilerView === 'list') st.listtab = profilerListTab; @@ -2007,11 +2009,18 @@ function navApply(st) { if ((st.sub || 'phone') === 'test') navApplyTest(st); } else if (st.tab === 'scp81') { scp81SwitchSubtab(st.sub || 'listener'); + } else if (st.tab === 'pysim') { + pysimSwitchSubtab(st.sub || 'files'); } else if (st.tab === 'cards') { navApplyCards(st); } else if (st.tab === 'profiler') { navApplyProfiler(st); } + } catch (e) { + // a replay must never leave the app half-switched: fall back to the + // default tab (the console carries the original failure) + if (typeof console !== 'undefined') console.warn('navApply failed:', e); + try { switchTab('c-apdu'); } catch (e2) { /* the default view failed too */ } } finally { _navApplying = false; } @@ -2128,6 +2137,9 @@ function cApduSwitchSubtab(name) { // Nested names select the Parser pill first, so callers can keep // addressing the panels directly (e.g. after a successful send). if (name === 'parse' || name === 'response') { + // Set the target sub first: the recursion's own parserSwitchSubtab + // then renders it, so the two records collapse into one entry. + _parserSub = name; cApduSwitchSubtab('parser'); parserSwitchSubtab(name); return; @@ -13595,6 +13607,7 @@ function cardsSetFormMode() { // "Add preset" in the list view: reset the form and open the editor. function cardsNew() { _cardsEditIdx = null; + _cardsEditId = null; cardsClearForm(); cardsSetFormMode(); cardsShowEditor(); @@ -13697,6 +13710,7 @@ function cardsEdit(i) { const c = cards[i]; if (!c) return; _cardsEditIdx = i; + _cardsEditId = c.id || null; const set = (id, value) => { const el = document.getElementById('cards-' + id); if (el) el.value = value == null ? '' : value; @@ -13718,6 +13732,7 @@ function cardsEdit(i) { function cardsCancelEdit() { navReplace(); _cardsEditIdx = null; + _cardsEditId = null; cardsSetFormMode(); cardsClearForm(); cardsShowList(); @@ -13741,6 +13756,7 @@ async function cardsRemove(i) { _ramCardIdx = ramCardIdxAfterRemove(_ramCardIdx, i); if (_cardsEditIdx === i) _cardsEditIdx = null; else if (_cardsEditIdx !== null && _cardsEditIdx > i) _cardsEditIdx--; + if (_cardsEditId === c.id) _cardsEditId = null; await cardsFetch(); } @@ -14193,6 +14209,7 @@ let pysimCmdHistIdx = -1; let _pysimAutoConnectAttempted = false; function pysimSwitchSubtab(name) { + _pysimSub = name; document.querySelectorAll('.pysim-subtab').forEach(btn => { const isActive = btn.dataset.pysimSub === name; btn.classList.toggle('bg-blue-600', isActive); @@ -14208,6 +14225,7 @@ function pysimSwitchSubtab(name) { pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager'}[name] || 'card-reader'; setHelpAnchor(pysimHelpAnchor); if (name === 'files') pysimFsFitTree(); + navRecord(); } function pysimCmdKeydown(e) { diff --git a/frontend/sw.js b/frontend/sw.js index d320d8a..49d20f5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v409'; +const CACHE = 'simple-v410'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/nav.test.js b/frontend/tests/nav.test.js index d98b6b7..8bf5011 100644 --- a/frontend/tests/nav.test.js +++ b/frontend/tests/nav.test.js @@ -44,12 +44,42 @@ test('navStateKey canonicalises the view state', () => { } }); +test('navCurrent builds the state from the view variables', () => { + // the module view variables + the one helper it calls, stubbed + eval(`var _activeTab = 'c-apdu', _scp80Sub = 'sp', _capduSub = 'sim', _parserSub = 'parse', + _phoneSub = 'phone', _scp81Sub = 'listener', _pysimSub = 'files', _cardsView = 'list', + _cardsEditId = null, _testView = 'suites', _testScriptId = null, _navSuite = null, + profilerView = 'list', profilerListTab = 'profiles', profilerEditId = null, + snapshotViewId = null; + function testCurrentSuite() { return _navSuite; }`); + eval(extractFunc(html, 'navCurrent')); + _activeTab = 'scp80'; _scp80Sub = 'ram'; + assert.deepStrictEqual(navCurrent(), { tab: 'scp80', sub: 'ram' }); + _activeTab = 'c-apdu'; _capduSub = 'parser'; _parserSub = 'response'; + assert.deepStrictEqual(navCurrent(), { tab: 'c-apdu', sub: 'parser', parser: 'response' }); + _activeTab = 'phone'; _phoneSub = 'test'; _testView = 'script'; + _navSuite = { id: 'S' }; _testScriptId = 'C'; + assert.deepStrictEqual(navCurrent(), + { tab: 'phone', sub: 'test', view: 'script', suite: 'S', script: 'C' }); + _activeTab = 'pysim'; _pysimSub = 'apdu'; + assert.deepStrictEqual(navCurrent(), { tab: 'pysim', sub: 'apdu' }); + _activeTab = 'cards'; _cardsView = 'edit'; _cardsEditId = 'P'; + assert.deepStrictEqual(navCurrent(), { tab: 'cards', view: 'edit', preset: 'P' }); + // a new-preset editor carries no id + _cardsEditId = null; + assert.deepStrictEqual(navCurrent(), { tab: 'cards', view: 'edit' }); + _activeTab = 'profiler'; profilerView = 'snapshot'; snapshotViewId = 'N'; + assert.deepStrictEqual(navCurrent(), { tab: 'profiler', view: 'snapshot', item: 'N' }); + profilerView = 'list'; profilerListTab = 'custom'; + assert.deepStrictEqual(navCurrent(), { tab: 'profiler', view: 'list', listtab: 'custom' }); +}); + test('the view switches record history, the back buttons replace it', () => { // every user-facing switch records its view for (const fn of ['switchTab', 'scp80SwitchSubtab', 'cApduSwitchSubtab', 'parserSwitchSubtab', 'phoneSwitchSubtab', 'scp81SwitchSubtab', - 'cardsShowList', 'cardsShowEditor', 'testViewShow', 'profilerSetView', - 'profilerListSwitch']) { + 'pysimSwitchSubtab', 'cardsShowList', 'cardsShowEditor', 'testViewShow', + 'profilerSetView', 'profilerListSwitch']) { assert.match(extractFunc(html, fn), /navRecord\(\)/, fn + ' must record'); } // the view variables the state is built from @@ -58,8 +88,15 @@ test('the view switches record history, the back buttons replace it', () => { assert.match(extractFunc(html, 'cApduSwitchSubtab'), /_capduSub = name/); assert.match(extractFunc(html, 'phoneSwitchSubtab'), /_phoneSub = name/); assert.match(extractFunc(html, 'scp81SwitchSubtab'), /_scp81Sub = name/); + assert.match(extractFunc(html, 'pysimSwitchSubtab'), /_pysimSub = name/); assert.match(extractFunc(html, 'cardsShowList'), /_cardsView = 'list'/); assert.match(extractFunc(html, 'cardsShowEditor'), /_cardsView = 'edit'/); + assert.match(extractFunc(html, 'cardsEdit'), /_cardsEditId = c\.id/); + assert.match(extractFunc(html, 'cardsNew'), /_cardsEditId = null/); + // the nested parser switch must not record two entries (the target sub is + // set before the recursion) + const capdu = extractFunc(html, 'cApduSwitchSubtab'); + assert.match(capdu, /_parserSub = name;\s*cApduSwitchSubtab\('parser'\)/); // the in-app Cancel/Back (and the profiler save) replace their entry for (const fn of ['cardsCancelEdit', 'profilerBackToList', 'testScriptBack', 'profilerSaveProfile']) { @@ -70,8 +107,15 @@ test('the view switches record history, the back buttons replace it', () => { assert.match(html, /history\.pushState\(/); assert.match(html, /history\.replaceState\(/); assert.match(html, /navSeed\(\);/); - assert.match(extractFunc(html, 'navApply'), /_navApplying = true/); - assert.match(extractFunc(html, 'navApply'), /switchTab\(/); + const apply = extractFunc(html, 'navApply'); + assert.match(apply, /_navApplying = true/); + assert.match(apply, /switchTab\(/); + assert.match(apply, /pysimSwitchSubtab\(/); + assert.match(apply, /navApplyTest\(/); + assert.match(apply, /navApplyCards\(/); + assert.match(apply, /navApplyProfiler\(/); + // a throwing replay falls back instead of escaping the popstate handler + assert.match(apply, /catch \(e\)/); for (const fn of ['navApplyTest', 'navApplyCards', 'navApplyProfiler', 'testBackToSuites']) { assert.match(html, new RegExp('function ' + fn + '\\('), fn + ' must exist'); } diff --git a/pyproject.toml b/pyproject.toml index a25f6df..a20d909 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.22.5" +version = "3.22.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 fb1c0fd..dd43f2d 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.22.5' +VERSION = '3.22.6' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE