Full path from MF or an ADF; the parent DF must be defined here first.
The parent may be any DF path — a standard DF from the tree or a custom DF, any depth; parents not yet seen in the tree are marked ⚠.
@@ -6317,6 +6318,7 @@ function pysimCustomInject(node) {
const fid = pysimCustomFid(c);
const existing = (node.children || []).find(ch => (ch.fid || '').toUpperCase() === fid);
if (existing) {
+ if (existing.modelName === undefined) existing.modelName = existing.name;
existing.name = c.name;
existing.kind = c.kind;
existing.custom = true;
@@ -6338,6 +6340,37 @@ function pysimCustomInject(node) {
}
}
+// Re-apply the custom-file list to the already-loaded tree: drop injected
+// nodes whose entry is gone, restore renamed model nodes, inject anew.
+function pysimCustomRefreshTree() {
+ if (!pysimFsTreeRoot) return;
+ const prune = node => {
+ if (!node.children) return;
+ node.children = node.children.filter(ch => {
+ if (!ch.custom) return true;
+ if (pysimCustomFiles.some(c => c.path === ch.customPath)) return true;
+ if (ch.modelName !== undefined) {
+ ch.name = ch.modelName;
+ delete ch.modelName;
+ delete ch.custom;
+ delete ch.customPath;
+ delete ch.kind;
+ return true;
+ }
+ return false;
+ });
+ };
+ const walk = node => {
+ prune(node);
+ pysimCustomInject(node);
+ for (const ch of (node.children || [])) {
+ if (ch.isDir) walk(ch);
+ }
+ };
+ walk(pysimFsTreeRoot);
+ pysimFsRenderTree();
+}
+
let pysimFsSort = localStorage.getItem('otaman_fs_sort') === 'name' ? 'name' : 'fid';
function pysimFsSortChildren(children, mode) {
@@ -6583,6 +6616,23 @@ function pysimFsFindNode(name, root) {
return null;
}
+// Find a node by its canonical path ("MF/7F20/6F46", "ADF.USIM/6F07"), so
+// custom-file parents can be resolved against the loaded tree (standard DFs
+// included, at any depth). Returns null when the path is not loaded yet.
+function pysimFsFindNodeByPath(path) {
+ const want = pysimCustomNormPath(path);
+ if (!want || !pysimFsTreeRoot) return null;
+ const walk = node => {
+ if (pysimCustomNormPath(pysimFsNodePath(node)) === want) return node;
+ for (const c of (node.children || [])) {
+ const hit = walk(c);
+ if (hit) return hit;
+ }
+ return null;
+ };
+ return walk(pysimFsTreeRoot);
+}
+
async function pysimFsToggleDir(name) {
const node = pysimFsFindNode(name, pysimFsTreeRoot);
if (!node || !node.isDir) return;
@@ -8730,20 +8780,66 @@ function pysimCustomFid(entry) { return entry.path.split('/').pop(); }
function pysimCustomParent(entry) { return entry.path.split('/').slice(0, -1).join('/'); }
function pysimCustomRoot(entry) { return entry.path.split('/')[0]; }
+// Parent DF suggestions for the current root: the root, custom DFs and every
+// DF node known from the loaded file-manager tree (standard pySim files),
+// at any depth.
+function pysimCustomKnownDfPaths(root) {
+ const out = [];
+ const seen = new Set();
+ const add = p => { if (p && !seen.has(p)) { seen.add(p); out.push(p); } };
+ add(root);
+ for (const c of pysimCustomFiles) {
+ if (c.kind === 'df' && pysimCustomRoot(c) === root) add(c.path);
+ }
+ const walk = node => {
+ for (const ch of (node.children || [])) {
+ if (!ch.isDir) continue;
+ const p = pysimFsNodePath(ch);
+ if (p && p.split('/')[0] === root) add(p);
+ walk(ch);
+ }
+ };
+ if (pysimFsTreeRoot) walk(pysimFsTreeRoot);
+ return out.sort();
+}
+
+// Where a parent path comes from: root, a custom DF, a DF of the loaded tree,
+// a known non-DF (error), or unknown (allowed, flagged as a warning).
+function pysimCustomParentStatus(parentPath) {
+ const p = pysimCustomNormPath(parentPath);
+ if (!p || p.split('/').length <= 1) return {status: 'root', source: null};
+ const c = pysimCustomFiles.find(e => e.path === p);
+ if (c) return {status: c.kind === 'df' ? 'df' : 'not-df', source: 'custom'};
+ const node = (typeof pysimFsFindNodeByPath === 'function') ? pysimFsFindNodeByPath(p) : null;
+ if (node) return {status: node.isDir ? 'df' : 'not-df', source: 'tree'};
+ return {status: 'unknown', source: null};
+}
+
function pysimCustomValidate(root, parentPath, fid, name, entries, editIndex) {
- if (!CUSTOM_ROOTS.includes(root)) {
+ parentPath = pysimCustomNormPath(parentPath) || root;
+ // a bare FID chain typed without the root is completed from the selector
+ const first = parentPath.split('/')[0];
+ if (!CUSTOM_ROOTS.includes(first) && /^[0-9A-F]{4}$/.test(first)) {
+ parentPath = pysimCustomNormPath(root + '/' + parentPath);
+ }
+ if (!CUSTOM_ROOTS.includes(parentPath.split('/')[0])) {
return {error: t('Root must be MF, ADF.USIM or ADF.ISIM')};
}
+ const segs = parentPath.split('/');
+ if (segs.length > 1 && !segs.slice(1).every(s => /^[0-9A-F]{4}$/.test(s))) {
+ return {error: t('Parent must be the root or a chain of 4-hex FIDs (e.g. MF/7F20)')};
+ }
fid = (fid || '').trim().toUpperCase();
if (!/^[0-9A-F]{4}$/.test(fid)) return {error: t('FID must be 4 hex characters')};
const kind = pysimCustomKindForName(name);
if (!kind) return {error: t('Alias must start with "EF." or "DF."')};
- if (parentPath !== root) {
- const idx = entries.findIndex((e, i) => i !== editIndex && e.path === parentPath);
- if (idx < 0) return {error: t('Parent DF is not defined as a custom file:') + ' ' + parentPath};
- if (entries[idx].kind !== 'df') return {error: t('Parent is not a DF:') + ' ' + parentPath};
+ // A parent known here to be an EF is an error; a parent that is a standard
+ // DF (or not seen yet) is fine - the tree resolves it later.
+ const parent = entries.find((e, i) => i !== editIndex && e.path === parentPath);
+ if (parent && parent.kind !== 'df') {
+ return {error: t('Parent is not a DF:') + ' ' + parentPath};
}
- const path = (parentPath === root ? root : parentPath) + '/' + fid;
+ const path = parentPath + '/' + fid;
if (entries.some((e, i) => i !== editIndex && e.path === path)) {
return {error: t('File already defined:') + ' ' + path};
}
@@ -8836,25 +8932,36 @@ function pysimCustomRenderRoots() {
}
function pysimCustomRenderParents() {
- const sel = document.getElementById('pysim-cf-parent');
- if (!sel) return;
+ const list = document.getElementById('pysim-cf-parent-list');
+ if (!list) return;
const root = document.getElementById('pysim-cf-root').value || CUSTOM_ROOTS[0];
- const prev = sel.value;
- let html = '
' + esc(t('— root —')) + ' ';
- for (const c of pysimCustomFiles) {
- if (c.kind !== 'df' || pysimCustomRoot(c) !== root) continue;
- html += '
' + esc(c.path + ' → ' + c.name) + ' ';
- }
- sel.innerHTML = html;
- if (prev && (prev === root || pysimCustomFiles.some(c => c.path === prev && c.kind === 'df'))) {
- sel.value = prev;
+ let html = '';
+ for (const p of pysimCustomKnownDfPaths(root)) {
+ const c = pysimCustomFiles.find(e => e.path === p && e.kind === 'df');
+ html += '
' + esc(c ? p + ' → ' + c.name : p) + ' ';
}
+ list.innerHTML = html;
}
function pysimCustomRootChanged() {
+ const sel = document.getElementById('pysim-cf-root');
+ const inp = document.getElementById('pysim-cf-parent');
+ const first = pysimCustomNormPath(inp && inp.value).split('/')[0];
+ if (CUSTOM_ROOTS.includes(first) && first !== sel.value) inp.value = sel.value;
pysimCustomRenderParents();
}
+// Keep the root selector (and its suggestions) in step with a typed parent path.
+function pysimCustomParentInput() {
+ const sel = document.getElementById('pysim-cf-root');
+ const inp = document.getElementById('pysim-cf-parent');
+ const first = pysimCustomNormPath(inp && inp.value).split('/')[0];
+ if (CUSTOM_ROOTS.includes(first) && sel.value !== first) {
+ sel.value = first;
+ pysimCustomRenderParents();
+ }
+}
+
function pysimCustomSubmit() {
const root = document.getElementById('pysim-cf-root').value;
const parentPath = document.getElementById('pysim-cf-parent').value || root;
@@ -8863,6 +8970,11 @@ function pysimCustomSubmit() {
const name = nameEl.value.trim();
const res = pysimCustomValidate(root, parentPath, fid, name, pysimCustomFiles, pysimCustomEditIndex);
if (res.error) { alert(res.error); return; }
+ const parent = pysimCustomParent({path: res.path});
+ if (pysimCustomParentStatus(parent).status === 'not-df') {
+ alert(t('Parent is not a DF:') + ' ' + parent);
+ return;
+ }
if (pysimCustomEditIndex !== null) {
const oldPath = pysimCustomFiles[pysimCustomEditIndex].path;
if (oldPath !== res.path) pysimCustomRewriteDescendants(pysimCustomFiles, oldPath, res.path);
@@ -8873,6 +8985,7 @@ function pysimCustomSubmit() {
pysimCustomSave();
pysimCustomRender();
pysimCustomEditCancel();
+ pysimCustomRefreshTree();
}
function pysimCustomEdit(i) {
@@ -8919,6 +9032,7 @@ function pysimCustomRemove(i) {
}
pysimCustomSave();
pysimCustomRender();
+ pysimCustomRefreshTree();
}
function pysimCustomRender() {
@@ -8936,8 +9050,11 @@ function pysimCustomRender() {
}
for (let i = 0; i < pysimCustomFiles.length; i++) {
const c = pysimCustomFiles[i];
+ const warn = pysimCustomParentStatus(pysimCustomParent(c)).status === 'unknown'
+ ? '
⚠ '
+ : '';
html += '
';
- html += '' + esc(c.path) + ' → ' + esc(c.name) + ' ';
+ html += '' + warn + esc(c.path) + ' → ' + esc(c.name) + ' ';
html += '(' + esc(c.kind.toUpperCase()) + ') ';
html += '';
html += '' + esc(t('Edit')) + ' ';
@@ -8969,6 +9086,7 @@ async function pysimCustomImport(text) {
pysimCustomFiles = res.files;
pysimCustomSave();
pysimCustomRender();
+ pysimCustomRefreshTree();
const el = document.getElementById('pysim-cf-io');
el.value = 'Imported ' + added + ' file(s)' + (res.dropped ? ', dropped ' + res.dropped : '') + '.';
setTimeout(() => { el.classList.add('hidden'); }, 2000);
@@ -11309,14 +11427,14 @@ const LANG_RU = {
'Parent DF': 'Родительский DF',
'FID (4 hex)': 'FID (4 hex)',
'Alias (EF. / DF.)': 'Псевдоним (EF. / DF.)',
- 'Full path from MF or an ADF; the parent DF must be defined here first.': 'Полный путь от MF или ADF; родительский DF должен быть создан здесь заранее.',
+ 'The parent may be any DF path — a standard DF from the tree or a custom DF, any depth; parents not yet seen in the tree are marked ⚠.': 'Родителем может быть любой путь DF — стандартный DF из дерева или пользовательский DF, любой вложенности; DF, ещё не встречавшиеся в дереве, помечаются ⚠.',
'Root must be MF, ADF.USIM or ADF.ISIM': 'Корень должен быть MF, ADF.USIM или ADF.ISIM',
'FID must be 4 hex characters': 'FID должен состоять из 4 hex-символов',
'Alias must start with "EF." or "DF."': 'Псевдоним должен начинаться с «EF.» или «DF.»',
- 'Parent DF is not defined as a custom file:': 'Родительский DF не создан как пользовательский файл:',
+ 'Parent must be the root or a chain of 4-hex FIDs (e.g. MF/7F20)': 'Родитель — корень или цепочка 4-hex FID (например, MF/7F20)',
'Parent is not a DF:': 'Родитель не является DF:',
+ 'Parent DF not seen in the tree yet': 'Родительский DF ещё не встречался в дереве',
'File already defined:': 'Файл уже задан:',
- '— root —': '— корень —',
'and': 'и',
'child file(s)?': 'дочерних файлов?',
'Dropped invalid legacy entries:': 'Отброшено некорректных старых записей:',
diff --git a/frontend/sw.js b/frontend/sw.js
index 59f95a8..9dda01f 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v175';
+const CACHE = 'otaman-v176';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/custom_files.test.js b/frontend/tests/custom_files.test.js
index 465c5cb..7d82950 100644
--- a/frontend/tests/custom_files.test.js
+++ b/frontend/tests/custom_files.test.js
@@ -21,17 +21,18 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
-let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\n';
+let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\nvar pysimFsTreeRoot = null;\n';
for (const fn of ['pysimCustomNormPath', 'pysimCustomKindForName', 'pysimCustomFid',
- 'pysimCustomParent', 'pysimCustomRoot', 'pysimCustomValidate',
- 'pysimCustomRewriteDescendants', 'pysimCustomNormalizeEntries', 'pysimCustomSave',
- 'pysimCustomRenderRoots', 'pysimCustomRenderParents', 'pysimCustomRootChanged',
- 'pysimCustomSubmit', 'pysimCustomEdit', 'pysimCustomEditCancel',
- 'pysimCustomRemove', 'pysimCustomRender']) {
+ 'pysimCustomParent', 'pysimCustomRoot', 'pysimCustomKnownDfPaths', 'pysimCustomParentStatus',
+ 'pysimCustomValidate', 'pysimCustomRewriteDescendants', 'pysimCustomNormalizeEntries',
+ 'pysimCustomSave', 'pysimCustomRenderRoots', 'pysimCustomRenderParents',
+ 'pysimCustomRootChanged', 'pysimCustomParentInput', 'pysimCustomSubmit',
+ 'pysimCustomEdit', 'pysimCustomEditCancel', 'pysimCustomRemove', 'pysimCustomRender',
+ 'pysimFsNodePath', 'pysimFsFindNodeByPath', 'pysimCustomInject', 'pysimCustomRefreshTree']) {
code += extractFunc(html, fn) + '\n';
}
code += html.match(/const CUSTOM_ROOTS = \[[^\]]*\];/)[0].replace('const ', 'var ') + '\n';
-code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
+code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimFsRenderTree = () => {};\n';
eval(code);
function fakeEl(id) {
@@ -53,6 +54,7 @@ function setup(entries) {
const els = {
'pysim-cf-root': fakeEl('pysim-cf-root'),
'pysim-cf-parent': fakeEl('pysim-cf-parent'),
+ 'pysim-cf-parent-list': fakeEl('pysim-cf-parent-list'),
'pysim-cf-fid': fakeEl('pysim-cf-fid'),
'pysim-cf-name': fakeEl('pysim-cf-name'),
'pysim-cf-list': fakeEl('pysim-cf-list'),
@@ -73,6 +75,7 @@ function setup(entries) {
pysimCustomFiles = (entries || []).map(e => Object.assign({}, e));
pysimCustomEditIndex = null;
_pysimCustomDropped = 0;
+ pysimFsTreeRoot = null;
pysimCustomRenderRoots();
pysimCustomRenderParents();
return els;
@@ -115,12 +118,24 @@ test('migration resolves legacy relative paths and drops the unresolvable', () =
assert.strictEqual(res.dropped, 2);
});
-test('validation requires a defined parent DF and a valid FID/alias', () => {
+test('validation accepts standard/unknown parents but rejects known EFs', () => {
const files = [{ path: 'MF/A153', name: 'DF.A1', kind: 'df' }];
- // parent not defined
- assert.match(pysimCustomValidate('MF', 'MF/A999', '6F46', 'EF.SPN', files, null).error, /not defined/);
+ // a parent that is not a custom entry is accepted: it may be a standard DF
+ // from the card model, or simply not seen in the tree yet
+ const unknown = pysimCustomValidate('MF', 'MF/A999', '6F46', 'EF.SPN', files, null);
+ assert.strictEqual(unknown.error, null);
+ assert.strictEqual(unknown.path, 'MF/A999/6F46');
// parent defined -> ok
assert.strictEqual(pysimCustomValidate('MF', 'MF/A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
+ // a bare FID chain typed without the root is completed from the selector
+ assert.strictEqual(pysimCustomValidate('MF', 'A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
+ // deep chains are fine
+ assert.strictEqual(pysimCustomValidate('MF', 'MF/7F20/5F01', '6F46', 'EF.DEEP', [], null).path, 'MF/7F20/5F01/6F46');
+ // a parent known here to be an EF is rejected
+ assert.match(pysimCustomValidate('MF', 'MF/A153/6F46', '1234', 'EF.X',
+ files.concat([{ path: 'MF/A153/6F46', name: 'EF.OTHER', kind: 'ef' }]), null).error, /not a DF/);
+ // parent segments must be 4-hex FIDs
+ assert.match(pysimCustomValidate('MF', 'MF/FOO', '6F46', 'EF.SPN', files, null).error, /4-hex/);
// bad FID
assert.match(pysimCustomValidate('MF', 'MF', '6F4', 'EF.SPN', files, null).error, /4 hex/);
// bad alias
@@ -135,21 +150,62 @@ test('validation requires a defined parent DF and a valid FID/alias', () => {
assert.match(pysimCustomValidate('MFX', 'MFX', '6F46', 'EF.SPN', files, null).error, /Root/);
});
-test('root and parent selectors list roots and defined DFs', () => {
+test('root and parent suggestions list roots, custom DFs and tree DFs', () => {
const els = setup([
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
{ path: 'ADF.USIM/6F07', name: 'EF.IMSI', kind: 'ef' },
]);
assert.deepStrictEqual(els['pysim-cf-root'].options(), ['MF', 'ADF.USIM', 'ADF.ISIM']);
- const parents = els['pysim-cf-parent'].options();
+ let parents = els['pysim-cf-parent-list'].options();
assert.ok(parents.includes('MF'));
assert.ok(parents.includes('MF/A153'));
assert.ok(!parents.includes('MF/A153/4954'), 'EFs are not parent options');
+ // DFs known from the loaded file tree are suggested, at any depth
+ pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
+ const df = { name: 'DF.TELECOM', fid: '7F10', isDir: true, parent: pysimFsTreeRoot, children: [] };
+ const sub = { name: 'DF.SUB', fid: '5F01', isDir: true, parent: df, children: [] };
+ df.children = [sub];
+ pysimFsTreeRoot.children = [df];
+ pysimCustomRenderParents();
+ parents = els['pysim-cf-parent-list'].options();
+ assert.ok(parents.includes('MF/7F10'));
+ assert.ok(parents.includes('MF/7F10/5F01'));
// the ADF root is always a valid parent
els['pysim-cf-root'].value = 'ADF.USIM';
pysimCustomRootChanged();
- assert.deepStrictEqual(els['pysim-cf-parent'].options(), ['ADF.USIM']);
+ assert.deepStrictEqual(els['pysim-cf-parent-list'].options(), ['ADF.USIM']);
+});
+
+test('parent status classifies root, custom, tree and unknown parents', () => {
+ setup([
+ { path: 'MF/A153', name: 'DF.A1', kind: 'df' },
+ { path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
+ ]);
+ assert.strictEqual(pysimCustomParentStatus('MF').status, 'root');
+ assert.strictEqual(pysimCustomParentStatus('MF/A153').status, 'df');
+ assert.strictEqual(pysimCustomParentStatus('MF/A153/4954').status, 'not-df');
+ assert.strictEqual(pysimCustomParentStatus('MF/FFFF').status, 'unknown');
+ pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
+ const df = { name: 'DF.GSM', fid: '7F20', isDir: true, parent: pysimFsTreeRoot, children: [] };
+ df.children = [{ name: 'EF.SPN', fid: '6F46', isDir: false, parent: df, children: null }];
+ pysimFsTreeRoot.children = [df];
+ assert.strictEqual(pysimCustomParentStatus('MF/7F20').status, 'df');
+ assert.strictEqual(pysimCustomParentStatus('MF/7F20/6F46').status, 'not-df');
+ assert.strictEqual(pysimCustomParentStatus('MF/7F20/9999').status, 'unknown');
+});
+
+test('import keeps canonical entries whose parent is outside the custom list', () => {
+ const res = pysimCustomNormalizeEntries([
+ { path: '3F00/7f20/5f01/6f46', name: 'EF.DEEP' },
+ { path: 'mf/ffff/6f46', name: 'EF.ORPHAN' },
+ { path: 'MF/6F46', name: 'NOPE' },
+ ], []);
+ assert.deepStrictEqual(res.files, [
+ { path: 'MF/7F20/5F01/6F46', name: 'EF.DEEP', kind: 'ef' },
+ { path: 'MF/FFFF/6F46', name: 'EF.ORPHAN', kind: 'ef' },
+ ]);
+ assert.strictEqual(res.dropped, 1);
});
test('submit adds files under the root and under a defined DF', () => {
@@ -165,13 +221,17 @@ test('submit adds files under the root and under a defined DF', () => {
pysimCustomSubmit();
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
['MF/6F46', 'MF/A153', 'MF/A153/4954']);
- // adding under an undefined parent is rejected
- fill(els, 'MF', 'MF', '1111', 'EF.X');
- els['pysim-cf-parent'].value = 'MF/A153';
- els['pysim-cf-fid'].value = '2222';
- els['pysim-cf-name'].value = 'BAD';
+ // a parent that is not a custom entry is allowed (it may be a standard DF
+ // the tree has not loaded yet)
+ fill(els, 'MF', 'MF/A153', '2222', 'EF.ORPHAN');
pysimCustomSubmit();
- assert.ok(globalThis.alertCalls.length === 1);
+ assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
+ ['MF/6F46', 'MF/A153', 'MF/A153/4954', 'MF/A153/2222']);
+ assert.strictEqual(globalThis.alertCalls.length, 0);
+ // a parent known here to be an EF is rejected
+ fill(els, 'MF', 'MF/A153/4954', '3333', 'EF.NOPE');
+ pysimCustomSubmit();
+ assert.deepStrictEqual(globalThis.alertCalls, ['Parent is not a DF: MF/A153/4954']);
});
test('editing a DF FID rewrites its descendants', () => {
diff --git a/frontend/tests/custom_inject.test.js b/frontend/tests/custom_inject.test.js
index a5ae495..f1caa80 100644
--- a/frontend/tests/custom_inject.test.js
+++ b/frontend/tests/custom_inject.test.js
@@ -21,10 +21,11 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
-let code = 'var pysimCustomFiles = [];\n';
-for (const fn of ['pysimFsNodePath', 'pysimCustomInject', 'pysimCustomParent', 'pysimCustomFid']) {
+let code = 'var pysimCustomFiles = [];\nvar pysimFsTreeRoot = null;\n';
+for (const fn of ['pysimFsNodePath', 'pysimCustomInject', 'pysimCustomRefreshTree', 'pysimCustomParent', 'pysimCustomFid']) {
code += extractFunc(html, fn) + '\n';
}
+code += 'globalThis.pysimFsRenderTree = () => {};\n';
eval(code);
function node(name, fid, parent, children) {
@@ -100,3 +101,25 @@ test('injected DFs are directories and can host their own children', () => {
assert.strictEqual(df.children.length, 1);
assert.strictEqual(df.children[0].name, 'EF.UNDER-NEW');
});
+
+test('refresh restores renamed model nodes and drops injected ones', () => {
+ const t = tree();
+ pysimFsTreeRoot = t.mf;
+ pysimCustomFiles = [{ path: 'MF/5F03/6F46', name: 'EF.RENAMED', kind: 'ef' }];
+ pysimCustomRefreshTree();
+ assert.strictEqual(t.dfC.children[0].name, 'EF.RENAMED');
+ assert.strictEqual(t.dfC.children[0].custom, true);
+ // deleting the entry brings the model name back
+ pysimCustomFiles = [];
+ pysimCustomRefreshTree();
+ assert.strictEqual(t.dfC.children[0].name, 'EF.X');
+ assert.ok(!t.dfC.children[0].custom);
+ assert.strictEqual(t.dfC.children[0].modelName, undefined);
+ // injected nodes disappear together with their entry
+ pysimCustomFiles = [{ path: 'MF/5F10', name: 'DF.NEW', kind: 'df' }];
+ pysimCustomRefreshTree();
+ assert.ok(t.mf.children.some(c => c.fid === '5F10'));
+ pysimCustomFiles = [];
+ pysimCustomRefreshTree();
+ assert.ok(!t.mf.children.some(c => c.fid === '5F10'));
+});
diff --git a/pyproject.toml b/pyproject.toml
index 41cb073..7075f93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
-version = "2.2.10"
+version = "2.2.11"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py
index c96ae83..02a0a34 100644
--- a/pysim_otaman_server/server.py
+++ b/pysim_otaman_server/server.py
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
-VERSION = '2.2.10'
+VERSION = '2.2.11'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE