file manager: path-based selection + ADF AID normalization
Make the DF context from the resolver actually reach the live views: - pysimFsSelected now stores the canonical path (pysimFsNodePath), not the node name; pysimFsRenderNode emits the path in the click/toggle handlers; every consumer resolves via pysimFsFindNodeByPath. The name-based pysimFsFindNode is removed - two same-named files under different DFs (EF.ECC/EF.AD, DF.PHONEBOOK copies) are now distinct. - efDfFromPath maps ADF-rooted parent segments by RID (A0000000871002 -> ADF.USIM, A0000000871004 -> ADF.ISIM), so snapshot/profile paths rooted by AID resolve to the same DF key as the named form. - efDecEcc shares efEccCategory with efDecEccUsim (TS 24.008 Table 10.5.135d), so the SIM and USIM category displays can't drift. Frontend 1087 + Python 1090 tests green. (Version/sw bump deferred - this is part of the unreleased decoded-editing batch.)
This commit is contained in:
1 parent
8e0a97f025
commit
5ebcd0773d
7 files changed
+237
-91
No files matched your search
@@ -26,13 +26,13 @@ const FNS = [
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efEccCategory', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes',
|
||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
||||
'pysimFsDecodedHtml', 'pysimFsNodePath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
'pysimFsDecodedHtml', 'pysimFsNodePath', 'pysimFsFindNodeByPath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
||||
'pysimCustomNormPath',
|
||||
];
|
||||
@@ -46,9 +46,10 @@ for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_ES
|
||||
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += html.match(/const EF_ECC_CATEGORIES = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n';
|
||||
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\nvar pysimFsTreeRoot = null;\n';
|
||||
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
|
||||
eval(code);
|
||||
|
||||
@@ -107,7 +108,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: 'police' });
|
||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
|
||||
@@ -358,6 +359,23 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
|
||||
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
||||
});
|
||||
|
||||
test('EF.ECC (DF.GSM) decodes the Emergency Service Category like the USIM file', () => {
|
||||
// TS 51.011 10.3.27: the category byte is the TS 24.008 10.5.135d coding,
|
||||
// the same table the ADF.USIM file uses (bit n = service n)
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19F104')), { number: '911', esc: 'fire brigade' });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19F102')), { number: '911', esc: 'ambulance' });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19F109')), { number: '911', esc: 'police, marine guard' });
|
||||
// a byte without a known bit keeps its raw hex value
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19F180')), { number: '911', esc: '0x80' });
|
||||
// a record whose code is FF is still decoded (only an all-FF record is empty)
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('FFFF00')), { number: null, esc: '0x00' });
|
||||
// several 3-byte records
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19F10411F220')), {
|
||||
numbers: [{ number: '911', esc: 'fire brigade' }, { number: '112', esc: 'manual eCall' }],
|
||||
});
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('FFFFFFFF')), { empty: true });
|
||||
});
|
||||
|
||||
test('efDfFromPath reads the parent DF of a canonical path', () => {
|
||||
assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20');
|
||||
assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM');
|
||||
@@ -374,6 +392,22 @@ test('efDfFromPath reads the parent DF of a canonical path', () => {
|
||||
assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD');
|
||||
});
|
||||
|
||||
test('efDfFromPath folds an ADF AID root onto its symbolic ADF name', () => {
|
||||
// a snapshot/profile path roots an ADF by the DF name (the AID) the card
|
||||
// reports, not by "ADF.USIM" - the RID decides, whatever the PIX is
|
||||
assert.strictEqual(efDfFromPath('A0000000871002/6F07'), 'ADF.USIM');
|
||||
assert.strictEqual(efDfFromPath('A0000000871002FF49FF0589/6F07'), 'ADF.USIM');
|
||||
assert.strictEqual(efDfFromPath('a0000000871002ff49ff0589/6fad'), 'ADF.USIM');
|
||||
assert.strictEqual(efDfFromPath('A0000000871004/6F07'), 'ADF.ISIM');
|
||||
assert.strictEqual(efDfFromPath('A0000000871004FF49FF0589/6F07'), 'ADF.ISIM');
|
||||
// an unrelated AID is not an ADF name: it passes through as itself
|
||||
assert.strictEqual(efDfFromPath('A0000000090003FFFF/6F07'), 'A0000000090003FFFF');
|
||||
assert.strictEqual(efDfFromPath('F0414C46416001/6F07'), 'F0414C46416001');
|
||||
// the AID root reads the same as the symbolic name and the plain FID do
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('A0000000871002/6FB7')).fn, efDecEccUsim);
|
||||
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('MF/7F20/6FB7')).fn, efDecEcc);
|
||||
});
|
||||
|
||||
test('the decoder resolver is DF-first: EF.ECC is the SIM or the USIM file', () => {
|
||||
// EF.ECC collides in name AND FID: only the parent DF separates the
|
||||
// TS 51.011 transparent form from the TS 31.102 linear-fixed one
|
||||
@@ -463,6 +497,38 @@ test('efFlatten skips the all-FF empty marker', () => {
|
||||
assert.deepStrictEqual(efFlatten({ empty: true }), []);
|
||||
});
|
||||
|
||||
test('the file manager resolves two same-named files by their parent DF', () => {
|
||||
// EF.ECC exists in DF.GSM and ADF.USIM with the same name and FID; the
|
||||
// selection key is the canonical path, so each resolves to its own node
|
||||
const root = { name: 'MF', fid: '3F00', children: [], parent: null };
|
||||
const gsm = { name: 'DF.GSM', fid: '7F20', children: [], parent: root };
|
||||
const usim = { name: 'ADF.USIM', fid: '7FFF', children: [], parent: root };
|
||||
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: gsm };
|
||||
const usimEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: usim };
|
||||
root.children = [gsm, usim];
|
||||
gsm.children = [gsmEcc];
|
||||
usim.children = [usimEcc];
|
||||
pysimFsTreeRoot = root;
|
||||
assert.strictEqual(pysimFsFindNodeByPath('MF/7F20/6FB7'), gsmEcc);
|
||||
assert.strictEqual(pysimFsFindNodeByPath('ADF.USIM/6FB7'), usimEcc);
|
||||
// a path that is not loaded resolves to nothing (the callers fall back to
|
||||
// the path's own last segment / FID)
|
||||
assert.strictEqual(pysimFsFindNodeByPath('MF/7F10/6FB7'), null);
|
||||
// the decoded view's DF follows the resolved node: the USIM file is the
|
||||
// linear-fixed X+4 form, the DF.GSM copy the SIM transparent one
|
||||
pysimFsSelected = 'ADF.USIM/6FB7';
|
||||
const usimHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
|
||||
{ success: true, records: [{ num: 1, data: '19F1FF04' }] });
|
||||
assert.ok(usimHtml.includes('Emergency call code'));
|
||||
assert.ok(usimHtml.includes('fire brigade'));
|
||||
pysimFsSelected = 'MF/7F20/6FB7';
|
||||
const gsmHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
|
||||
{ success: true, data: '19F104' });
|
||||
assert.ok(gsmHtml.includes('Emergency category'));
|
||||
assert.ok(gsmHtml.includes('fire brigade'));
|
||||
assert.ok(!gsmHtml.includes('Emergency call code'));
|
||||
});
|
||||
|
||||
test('pysimFsDecodedHtml decodes EF.ECC by the selected node\'s parent DF', () => {
|
||||
// ADF.USIM's EF.ECC is the linear-fixed X+4 form (code/alpha/category)
|
||||
const usim = { name: 'ADF.USIM', children: [] };
|
||||
|
||||
@@ -52,7 +52,7 @@ const FNS = [
|
||||
'efDecRecordsHtml', 'efDecEditHtml',
|
||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNode', 'pysimFsNodePath', 'efDfFromPath',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
|
||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||
'gsm7TextToSeptets', 'gsm7Encode',
|
||||
@@ -63,7 +63,7 @@ let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimF
|
||||
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -253,30 +253,41 @@ test('changing a record input checks its block', () => {
|
||||
assert.strictEqual(cb.checked, true);
|
||||
});
|
||||
|
||||
test('pysimFsHasEncoder resolves the selected file by name or FID', () => {
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }, { name: 'EF.ALIAS', fid: '6f3a' }] };
|
||||
pysimFsSelected = 'EF.SPN';
|
||||
test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
|
||||
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||
const add = (name, fid) => {
|
||||
const n = { name: name, fid: fid, parent: pysimFsTreeRoot };
|
||||
pysimFsTreeRoot.children.push(n);
|
||||
return n;
|
||||
};
|
||||
add('EF.SPN', '6f46');
|
||||
add('EF.ALIAS', '6f3a');
|
||||
pysimFsSelected = 'MF/6F46';
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = 'EF.ALIAS';
|
||||
pysimFsSelected = 'MF/6F3A';
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
// a bare FID selection still resolves through the FID fallback
|
||||
pysimFsSelected = '6F46';
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = 'EF.LOCI';
|
||||
pysimFsSelected = 'MF/6F7E';
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
// the SIM service table shares 6F38 with UST but has no editor
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SST', fid: '6f38' }] };
|
||||
pysimFsSelected = 'EF.SST';
|
||||
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||
const sst = { name: 'EF.SST', fid: '6f38', parent: pysimFsTreeRoot };
|
||||
pysimFsTreeRoot.children.push(sst);
|
||||
pysimFsSelected = 'MF/6F38';
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
pysimFsSelected = null;
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
});
|
||||
|
||||
test('the file-manager resolver call sites pass the node\'s parent DF', () => {
|
||||
// the DF context comes from the node's canonical path; these five sites
|
||||
test('the file-manager resolver call sites pass the parent DF', () => {
|
||||
// the DF context comes from the selected node's canonical path (or the
|
||||
// selected path itself when the node is not loaded); these sites
|
||||
// gate/read/edit a selected file and must resolve the same entry
|
||||
assert.ok(html.includes('return !!efFindDecoder(node && node.name, fid, df);'));
|
||||
assert.ok(html.includes('return !!efFindEncoder(node && node.name, fid, df);'));
|
||||
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : '';"));
|
||||
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);"));
|
||||
assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,'));
|
||||
for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) {
|
||||
assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)),
|
||||
@@ -647,8 +658,9 @@ test('F2: untouched PNN items keep their TLV bytes through the DOM and the save'
|
||||
});
|
||||
|
||||
test('F12: a selection change during the read abandons the decoded edit', async () => {
|
||||
pysimFsSelected = 'EF.SPN';
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }] };
|
||||
pysimFsSelected = 'MF/6F46';
|
||||
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||
pysimFsTreeRoot.children.push({ name: 'EF.SPN', fid: '6f46', parent: pysimFsTreeRoot });
|
||||
const fakeEl = () => {
|
||||
const classes = new Set();
|
||||
return {
|
||||
@@ -681,7 +693,7 @@ test('F12: a selection change during the read abandons the decoded edit', async
|
||||
let resets = 0;
|
||||
globalThis.pysimFsResetEdit = () => { resets++; };
|
||||
const pending = pysimFsDecEdit();
|
||||
pysimFsSelected = 'EF.ADN';
|
||||
pysimFsSelected = 'MF/6F3A';
|
||||
release();
|
||||
await pending;
|
||||
assert.strictEqual(resets, 1);
|
||||
|
||||
@@ -29,11 +29,12 @@ let code = 'var pysimFsDecodedMode = false;\n'
|
||||
+ 'var pysimFsSelected = null;\n'
|
||||
+ 'var pysimFsTreeRoot = null;\n';
|
||||
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsNodePath',
|
||||
'efDfFromPath', 'pysimFsHasDecoder',
|
||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNodeByPath', 'pysimCustomNormPath',
|
||||
'pysimFsNodePath', 'efFidFromPath', 'efDfFromPath', 'pysimFsHasDecoder',
|
||||
'pysimFsSyncContentVisibility']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||
eval(code);
|
||||
|
||||
let events = [];
|
||||
@@ -84,8 +85,9 @@ function setup(opts) {
|
||||
pysimFsDecodedMode = !!opts.decoded;
|
||||
pysimFsEditMode = false;
|
||||
pysimFsEditData = null;
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [{ name: 'EF.IMSI', fid: '6f07' }] };
|
||||
pysimFsSelected = 'MF/6F07';
|
||||
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [] };
|
||||
pysimFsTreeRoot.children.push({ name: 'EF.IMSI', fid: '6f07', parent: pysimFsTreeRoot });
|
||||
globalThis.efFindDecoder = (name, fid) => {
|
||||
const n = (name || '').toUpperCase();
|
||||
const f = (fid || '').toLowerCase();
|
||||
@@ -206,12 +208,16 @@ test('Read decoded is ignored when the file has no decoder', () => {
|
||||
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
||||
});
|
||||
|
||||
test('pysimFsHasDecoder resolves the selected file by name or FID', () => {
|
||||
test('pysimFsHasDecoder resolves the selected file by its path or FID', () => {
|
||||
setup();
|
||||
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
|
||||
// alias name: the decoder is matched by the FID fallback
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
|
||||
pysimFsSelected = 'EF.ALIAS';
|
||||
pysimFsTreeRoot.children[0].parent = pysimFsTreeRoot;
|
||||
pysimFsSelected = 'MF/6F07';
|
||||
assert.strictEqual(pysimFsHasDecoder(), true);
|
||||
// a bare FID selection (an API caller) resolves by the FID fallback
|
||||
pysimFsSelected = '6F07';
|
||||
assert.strictEqual(pysimFsHasDecoder(), true);
|
||||
// unknown file -> no decoder
|
||||
globalThis.efFindDecoder = () => null;
|
||||
|
||||
@@ -24,6 +24,9 @@ function extractFunc(src, name) {
|
||||
let code = 'var pysimFsSelected = null;\n'
|
||||
+ 'var pysimFsDecodedMode = false;\n'
|
||||
+ 'var pysimFsTreeRoot = null;\n';
|
||||
code += extractFunc(html, 'efFidFromPath') + '\n';
|
||||
code += extractFunc(html, 'efDfFromPath') + '\n';
|
||||
code += extractFunc(html, 'pysimFsFallbackNode') + '\n';
|
||||
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
|
||||
code += extractFunc(html, 'pysimFsRead') + '\n';
|
||||
eval(code);
|
||||
@@ -53,7 +56,7 @@ function setup() {
|
||||
};
|
||||
globalThis.t = s => s;
|
||||
globalThis.esc = s => s;
|
||||
globalThis.pysimFsFindNode = () => ({ name: 'EF.IMSI', fid: '6f07' });
|
||||
globalThis.pysimFsFindNodeByPath = () => ({ name: 'EF.IMSI', fid: '6f07' });
|
||||
globalThis.pysimFsSelectBody = () => ({});
|
||||
globalThis.pysimFsShowError = (el, sw, err) => {
|
||||
errorShown = { sw, err };
|
||||
@@ -63,7 +66,7 @@ function setup() {
|
||||
if (fetchResult instanceof Error) throw fetchResult;
|
||||
return fetchResult;
|
||||
};
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
pysimFsSelected = 'ADF.USIM/6F07';
|
||||
pysimFsDecodedMode = false;
|
||||
pysimFsTreeRoot = { name: 'MF' };
|
||||
fetchResult = { success: true, sw: '9000', data: 'AABB' };
|
||||
@@ -92,6 +95,17 @@ test('a successful read fills and shows the content pane', async () => {
|
||||
assert.strictEqual(status.textContent, 'SW: 9000 OK');
|
||||
});
|
||||
|
||||
test('a path whose node is not loaded selects by its last segment and FID', async () => {
|
||||
setup();
|
||||
globalThis.pysimFsFindNodeByPath = () => null;
|
||||
let body = null;
|
||||
globalThis.pysimFsSelectBody = node => { body = node; return {}; };
|
||||
pysimFsSelected = 'MF/7F20/6FB7';
|
||||
const ok = await pysimFsRead();
|
||||
assert.strictEqual(ok, true);
|
||||
assert.deepStrictEqual(body, { name: '6FB7', fid: '6FB7' });
|
||||
});
|
||||
|
||||
test('a failed read keeps the content pane hidden', async () => {
|
||||
setup();
|
||||
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
|
||||
|
||||
@@ -23,6 +23,7 @@ function extractFunc(src, name) {
|
||||
|
||||
let code = 'var pysimFsSort = "fid";\n';
|
||||
code += extractFunc(html, 'pysimFsSortChildren') + '\n';
|
||||
code += extractFunc(html, 'pysimFsNodePath') + '\n';
|
||||
code += extractFunc(html, 'pysimFsRenderNode') + '\n';
|
||||
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
|
||||
eval(code);
|
||||
@@ -65,3 +66,27 @@ test('non-existing directory hides previously loaded children', () => {
|
||||
assert.ok(!out.includes('(empty)'), out);
|
||||
assert.ok(!out.includes('pysimFsToggleDir'), out);
|
||||
});
|
||||
|
||||
test('the row handlers carry the canonical path, so same-named files differ', () => {
|
||||
// EF.ECC lives in DF.GSM and ADF.USIM with the same name AND FID: the
|
||||
// onclick argument must be the path, never the ambiguous display name
|
||||
const root = { name: 'MF', fid: '3F00', isDir: true, exists: true, children: [], expanded: true, parent: null };
|
||||
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: true, children: [], expanded: true, parent: root };
|
||||
const usim = { name: 'ADF.USIM', fid: '7FFF', isDir: true, exists: true, children: [], expanded: true, parent: root };
|
||||
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: gsm };
|
||||
const usimEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: usim };
|
||||
root.children = [gsm, usim];
|
||||
gsm.children = [gsmEcc];
|
||||
usim.children = [usimEcc];
|
||||
assert.ok(pysimFsRenderNode(gsmEcc, 0).includes("pysimFsClickFile('MF/7F20/6FB7')"));
|
||||
assert.ok(pysimFsRenderNode(usimEcc, 0).includes("pysimFsClickFile('ADF.USIM/6FB7')"));
|
||||
assert.ok(pysimFsRenderNode(gsm, 0).includes("pysimFsToggleDir('MF/7F20')"));
|
||||
assert.ok(pysimFsRenderNode(usim, 0).includes("pysimFsToggleDir('ADF.USIM')"));
|
||||
});
|
||||
|
||||
test('pysimFsRenderNode passes pysimFsNodePath(node) to the click/toggle handlers', () => {
|
||||
const body = extractFunc(html, 'pysimFsRenderNode');
|
||||
assert.ok(body.includes('const nodePath = pysimFsNodePath(node);'));
|
||||
assert.match(body, /pysimFsToggleDir\(\\'' \+ esc\(nodePath\)/);
|
||||
assert.match(body, /pysimFsClickFile\(\\'' \+ esc\(nodePath\)/);
|
||||
});
|
||||
@@ -446,7 +446,8 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
|
||||
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
|
||||
assert.ok(!html.includes('pysim-fs-filename'));
|
||||
assert.ok(html.includes('let pysimFsSelected = null;'));
|
||||
assert.ok(html.includes('pysimFsSelected = name;'));
|
||||
assert.ok(html.includes('pysimFsSelected = node ? pysimFsNodePath(node) : path;'),
|
||||
'the selection is keyed by the canonical path, not the display name');
|
||||
assert.ok(!html.includes('pysimFsSelect()'));
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user