v3.22.6: the view-history review fixes
Findings from the review of the v3.22.5 router:
- the Card reader's pills (file manager / pySim command line / raw APDU)
join the router - the one view group the sweep missed: _pysimSub feeds the
state, pysimSwitchSubtab records, navCurrent/navApply know the tab;
- the nested parser switch (cApduSwitchSubtab('parse'|'response'), used by
the send-to-parser jump) sets _parserSub before recursing, so the
recursion's own parserSwitchSubtab renders the target and the two records
collapse into one history entry instead of two;
- navApply catches a failing replay and falls back to the default tab (the
console carries the failure) instead of letting the exception escape the
popstate handler with a stale _navState;
- the cards editor's recorded preset id comes from _cardsEditId (set by
cardsEdit, cleared by cardsNew/cardsCancelEdit/cardsRemove) instead of
cards[_cardsEditIdx], so a store refetch that shifts the array can no
longer record the wrong preset;
- nav.test.js: pysimSwitchSubtab added to the hook list, a navCurrent
state-building test (all tab branches), the apply-side assertions and the
single-entry parser check.
Tests: 843 frontend + 819 Python; sw cache simple-v410.
This commit is contained in:
+21
-3
@@ -1903,7 +1903,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -1917,7 +1917,9 @@ let _scp80Sub = 'sp';
|
|||||||
let _capduSub = 'sim';
|
let _capduSub = 'sim';
|
||||||
let _phoneSub = 'phone';
|
let _phoneSub = 'phone';
|
||||||
let _scp81Sub = 'listener';
|
let _scp81Sub = 'listener';
|
||||||
|
let _pysimSub = 'files';
|
||||||
let _cardsView = 'list';
|
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 _navState = null; // the state of the current history entry
|
||||||
let _navApplying = false; // replaying: the switch functions must not record
|
let _navApplying = false; // replaying: the switch functions must not record
|
||||||
let _navOk = true; // false when pushState is unavailable (file://)
|
let _navOk = true; // false when pushState is unavailable (file://)
|
||||||
@@ -1948,10 +1950,10 @@ function navCurrent() {
|
|||||||
if (_testView === 'script' && _testScriptId) st.script = _testScriptId;
|
if (_testView === 'script' && _testScriptId) st.script = _testScriptId;
|
||||||
}
|
}
|
||||||
} else if (_activeTab === 'scp81') st.sub = _scp81Sub;
|
} else if (_activeTab === 'scp81') st.sub = _scp81Sub;
|
||||||
|
else if (_activeTab === 'pysim') st.sub = _pysimSub;
|
||||||
else if (_activeTab === 'cards') {
|
else if (_activeTab === 'cards') {
|
||||||
st.view = _cardsView;
|
st.view = _cardsView;
|
||||||
const p = (_cardsEditIdx !== null && cards[_cardsEditIdx]) ? cards[_cardsEditIdx] : null;
|
if (_cardsView === 'edit' && _cardsEditId) st.preset = _cardsEditId;
|
||||||
if (_cardsView === 'edit' && p) st.preset = p.id || '';
|
|
||||||
} else if (_activeTab === 'profiler') {
|
} else if (_activeTab === 'profiler') {
|
||||||
st.view = profilerView;
|
st.view = profilerView;
|
||||||
if (profilerView === 'list') st.listtab = profilerListTab;
|
if (profilerView === 'list') st.listtab = profilerListTab;
|
||||||
@@ -2007,11 +2009,18 @@ function navApply(st) {
|
|||||||
if ((st.sub || 'phone') === 'test') navApplyTest(st);
|
if ((st.sub || 'phone') === 'test') navApplyTest(st);
|
||||||
} else if (st.tab === 'scp81') {
|
} else if (st.tab === 'scp81') {
|
||||||
scp81SwitchSubtab(st.sub || 'listener');
|
scp81SwitchSubtab(st.sub || 'listener');
|
||||||
|
} else if (st.tab === 'pysim') {
|
||||||
|
pysimSwitchSubtab(st.sub || 'files');
|
||||||
} else if (st.tab === 'cards') {
|
} else if (st.tab === 'cards') {
|
||||||
navApplyCards(st);
|
navApplyCards(st);
|
||||||
} else if (st.tab === 'profiler') {
|
} else if (st.tab === 'profiler') {
|
||||||
navApplyProfiler(st);
|
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 {
|
} finally {
|
||||||
_navApplying = false;
|
_navApplying = false;
|
||||||
}
|
}
|
||||||
@@ -2128,6 +2137,9 @@ function cApduSwitchSubtab(name) {
|
|||||||
// Nested names select the Parser pill first, so callers can keep
|
// Nested names select the Parser pill first, so callers can keep
|
||||||
// addressing the panels directly (e.g. after a successful send).
|
// addressing the panels directly (e.g. after a successful send).
|
||||||
if (name === 'parse' || name === 'response') {
|
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');
|
cApduSwitchSubtab('parser');
|
||||||
parserSwitchSubtab(name);
|
parserSwitchSubtab(name);
|
||||||
return;
|
return;
|
||||||
@@ -13595,6 +13607,7 @@ function cardsSetFormMode() {
|
|||||||
// "Add preset" in the list view: reset the form and open the editor.
|
// "Add preset" in the list view: reset the form and open the editor.
|
||||||
function cardsNew() {
|
function cardsNew() {
|
||||||
_cardsEditIdx = null;
|
_cardsEditIdx = null;
|
||||||
|
_cardsEditId = null;
|
||||||
cardsClearForm();
|
cardsClearForm();
|
||||||
cardsSetFormMode();
|
cardsSetFormMode();
|
||||||
cardsShowEditor();
|
cardsShowEditor();
|
||||||
@@ -13697,6 +13710,7 @@ function cardsEdit(i) {
|
|||||||
const c = cards[i];
|
const c = cards[i];
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
_cardsEditIdx = i;
|
_cardsEditIdx = i;
|
||||||
|
_cardsEditId = c.id || null;
|
||||||
const set = (id, value) => {
|
const set = (id, value) => {
|
||||||
const el = document.getElementById('cards-' + id);
|
const el = document.getElementById('cards-' + id);
|
||||||
if (el) el.value = value == null ? '' : value;
|
if (el) el.value = value == null ? '' : value;
|
||||||
@@ -13718,6 +13732,7 @@ function cardsEdit(i) {
|
|||||||
function cardsCancelEdit() {
|
function cardsCancelEdit() {
|
||||||
navReplace();
|
navReplace();
|
||||||
_cardsEditIdx = null;
|
_cardsEditIdx = null;
|
||||||
|
_cardsEditId = null;
|
||||||
cardsSetFormMode();
|
cardsSetFormMode();
|
||||||
cardsClearForm();
|
cardsClearForm();
|
||||||
cardsShowList();
|
cardsShowList();
|
||||||
@@ -13741,6 +13756,7 @@ async function cardsRemove(i) {
|
|||||||
_ramCardIdx = ramCardIdxAfterRemove(_ramCardIdx, i);
|
_ramCardIdx = ramCardIdxAfterRemove(_ramCardIdx, i);
|
||||||
if (_cardsEditIdx === i) _cardsEditIdx = null;
|
if (_cardsEditIdx === i) _cardsEditIdx = null;
|
||||||
else if (_cardsEditIdx !== null && _cardsEditIdx > i) _cardsEditIdx--;
|
else if (_cardsEditIdx !== null && _cardsEditIdx > i) _cardsEditIdx--;
|
||||||
|
if (_cardsEditId === c.id) _cardsEditId = null;
|
||||||
await cardsFetch();
|
await cardsFetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14193,6 +14209,7 @@ let pysimCmdHistIdx = -1;
|
|||||||
let _pysimAutoConnectAttempted = false;
|
let _pysimAutoConnectAttempted = false;
|
||||||
|
|
||||||
function pysimSwitchSubtab(name) {
|
function pysimSwitchSubtab(name) {
|
||||||
|
_pysimSub = name;
|
||||||
document.querySelectorAll('.pysim-subtab').forEach(btn => {
|
document.querySelectorAll('.pysim-subtab').forEach(btn => {
|
||||||
const isActive = btn.dataset.pysimSub === name;
|
const isActive = btn.dataset.pysimSub === name;
|
||||||
btn.classList.toggle('bg-blue-600', isActive);
|
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';
|
pysimHelpAnchor = {cmd:'pysim-cmdline', apdu:'raw-apdu', files:'file-manager'}[name] || 'card-reader';
|
||||||
setHelpAnchor(pysimHelpAnchor);
|
setHelpAnchor(pysimHelpAnchor);
|
||||||
if (name === 'files') pysimFsFitTree();
|
if (name === 'files') pysimFsFitTree();
|
||||||
|
navRecord();
|
||||||
}
|
}
|
||||||
|
|
||||||
function pysimCmdKeydown(e) {
|
function pysimCmdKeydown(e) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v409';
|
const CACHE = 'simple-v410';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -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', () => {
|
test('the view switches record history, the back buttons replace it', () => {
|
||||||
// every user-facing switch records its view
|
// every user-facing switch records its view
|
||||||
for (const fn of ['switchTab', 'scp80SwitchSubtab', 'cApduSwitchSubtab',
|
for (const fn of ['switchTab', 'scp80SwitchSubtab', 'cApduSwitchSubtab',
|
||||||
'parserSwitchSubtab', 'phoneSwitchSubtab', 'scp81SwitchSubtab',
|
'parserSwitchSubtab', 'phoneSwitchSubtab', 'scp81SwitchSubtab',
|
||||||
'cardsShowList', 'cardsShowEditor', 'testViewShow', 'profilerSetView',
|
'pysimSwitchSubtab', 'cardsShowList', 'cardsShowEditor', 'testViewShow',
|
||||||
'profilerListSwitch']) {
|
'profilerSetView', 'profilerListSwitch']) {
|
||||||
assert.match(extractFunc(html, fn), /navRecord\(\)/, fn + ' must record');
|
assert.match(extractFunc(html, fn), /navRecord\(\)/, fn + ' must record');
|
||||||
}
|
}
|
||||||
// the view variables the state is built from
|
// 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, 'cApduSwitchSubtab'), /_capduSub = name/);
|
||||||
assert.match(extractFunc(html, 'phoneSwitchSubtab'), /_phoneSub = name/);
|
assert.match(extractFunc(html, 'phoneSwitchSubtab'), /_phoneSub = name/);
|
||||||
assert.match(extractFunc(html, 'scp81SwitchSubtab'), /_scp81Sub = 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, 'cardsShowList'), /_cardsView = 'list'/);
|
||||||
assert.match(extractFunc(html, 'cardsShowEditor'), /_cardsView = 'edit'/);
|
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
|
// the in-app Cancel/Back (and the profiler save) replace their entry
|
||||||
for (const fn of ['cardsCancelEdit', 'profilerBackToList', 'testScriptBack',
|
for (const fn of ['cardsCancelEdit', 'profilerBackToList', 'testScriptBack',
|
||||||
'profilerSaveProfile']) {
|
'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\.pushState\(/);
|
||||||
assert.match(html, /history\.replaceState\(/);
|
assert.match(html, /history\.replaceState\(/);
|
||||||
assert.match(html, /navSeed\(\);/);
|
assert.match(html, /navSeed\(\);/);
|
||||||
assert.match(extractFunc(html, 'navApply'), /_navApplying = true/);
|
const apply = extractFunc(html, 'navApply');
|
||||||
assert.match(extractFunc(html, 'navApply'), /switchTab\(/);
|
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']) {
|
for (const fn of ['navApplyTest', 'navApplyCards', 'navApplyProfiler', 'testBackToSuites']) {
|
||||||
assert.match(html, new RegExp('function ' + fn + '\\('), fn + ' must exist');
|
assert.match(html, new RegExp('function ' + fn + '\\('), fn + ' must exist');
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.22.5"
|
version = "3.22.6"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user