ui: hide, don't clear, children of non-selectable DFs

- pysimFsLoadChildren failure now only marks exists=false and re-renders;
  loaded children stay in the node and are hidden by the renderer (red
  cross, no toggle, no (empty)), so a failed DF is re-attempted on the
  next probe/refresh instead of being short-circuited
- dropped the parent_sel-less /api/tree retry: error payloads now carry
  exists:false, so it doubled requests for every absent DF and re-selected
  the same FID without its parent (pySim probe_file fallback)
- probe walks strictly on exists===true and never collects files under a
  non-existing DF; no longer clears children on a failed EF select
- tests updated: single request per failed load, children untouched,
  200 exists:false marks absent without retry, render hides stale
  children, probe never fetches/selects them; SW cache v116 -> v117.
This commit is contained in:
2026-09-12 21:23:31 +03:00
parent 92271d6726
commit 85a66af7a5
5 changed files with 50 additions and 23 deletions
+4 -10
View File
@@ -5940,14 +5940,10 @@ async function pysimFsRefresh() {
async function pysimFsLoadChildren(node) { async function pysimFsLoadChildren(node) {
if (node.children) return; if (node.children) return;
try { try {
let body = { name: node.name, fid: node.fid }; const body = { name: node.name, fid: node.fid };
const parentSel = getParentSel(node); const parentSel = getParentSel(node);
if (parentSel) body.parent_sel = parentSel; if (parentSel) body.parent_sel = parentSel;
let data = await pysimFetch('/api/tree', body); const data = await pysimFetch('/api/tree', body);
if (!data || data.exists === false) {
body = { name: node.name, fid: node.fid };
data = await pysimFetch('/api/tree', body);
}
if (!data || data.exists === false || data.success === false || data.error) { if (!data || data.exists === false || data.success === false || data.error) {
throw new Error((data && data.error) || 'File not found'); throw new Error((data && data.error) || 'File not found');
} }
@@ -5965,7 +5961,6 @@ async function pysimFsLoadChildren(node) {
pysimCustomInject(node); pysimCustomInject(node);
} catch (e) { } catch (e) {
node.exists = false; node.exists = false;
node.children = [];
pysimFsRenderTree(); pysimFsRenderTree();
} }
} }
@@ -6007,7 +6002,7 @@ async function pysimFsProbeAll() {
if (probe.stop) return; if (probe.stop) return;
if (!child.isDir) continue; if (!child.isDir) continue;
await pysimFsLoadChildren(child); await pysimFsLoadChildren(child);
const exists = child.exists !== false; const exists = child.exists === true;
probe.dirs++; probe.dirs++;
count(exists); count(exists);
pysimFsRenderTree(); pysimFsRenderTree();
@@ -6019,7 +6014,7 @@ async function pysimFsProbeAll() {
const files = []; const files = [];
const collect = node => { const collect = node => {
for (const child of (node.children || [])) { for (const child of (node.children || [])) {
if (child.isDir) { if (child.exists !== false) collect(child); } if (child.isDir) { if (child.exists === true) collect(child); }
else files.push(child); else files.push(child);
} }
}; };
@@ -6031,7 +6026,6 @@ async function pysimFsProbeAll() {
const data = await pysimFetch('/api/select', pysimFsSelectBody(file)); const data = await pysimFetch('/api/select', pysimFsSelectBody(file));
const exists = !!(data && data.exists !== false && !data.error); const exists = !!(data && data.exists !== false && !data.error);
file.exists = exists; file.exists = exists;
if (!exists) file.children = [];
count(exists); count(exists);
probe.done++; probe.done++;
pysimFsProbeUi(t('Probing:') + ' ' + probe.done + '/' + probe.total, true); pysimFsProbeUi(t('Probing:') + ' ' + probe.done + '/' + probe.total, true);
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v116'; const CACHE = 'otaman-v117';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+16 -11
View File
@@ -54,9 +54,9 @@ test('tree error payload marks the directory as absent', async () => {
]; ];
await pysimFsLoadChildren(node); await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false); assert.strictEqual(node.exists, false);
assert.deepStrictEqual(node.children, []); assert.strictEqual(node.children, null);
assert.strictEqual(renders, 1); assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 2); assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].body.parent_sel, 'MF'); assert.strictEqual(calls[0].body.parent_sel, 'MF');
}); });
@@ -68,23 +68,28 @@ test('error payload without exists is not treated as an empty listing', async ()
]; ];
await pysimFsLoadChildren(node); await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, false); assert.strictEqual(node.exists, false);
assert.deepStrictEqual(node.children, []); assert.strictEqual(node.children, null);
assert.strictEqual(renders, 1); assert.strictEqual(renders, 1);
assert.strictEqual(calls.length, 1); assert.strictEqual(calls.length, 1);
}); });
test('retry without parent_sel succeeds and maps children', async () => { test('a 200 exists:false response marks the directory absent without retrying', async () => {
const node = setup(); const node = setup();
responses = [ responses = [{ exists: false }];
{ exists: false }, await pysimFsLoadChildren(node);
{ exists: true, children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] }, assert.strictEqual(node.exists, false);
]; assert.strictEqual(node.children, null);
assert.strictEqual(calls.length, 1);
});
test('a node with loaded children is not fetched again', async () => {
const node = setup();
node.exists = true;
node.children = [{ name: 'EF.UPLMNWLAN', fid: '4f42', isDir: false, exists: true }];
await pysimFsLoadChildren(node); await pysimFsLoadChildren(node);
assert.strictEqual(node.exists, true); assert.strictEqual(node.exists, true);
assert.strictEqual(node.children.length, 1); assert.strictEqual(node.children.length, 1);
assert.strictEqual(node.children[0].parent, node); assert.strictEqual(calls.length, 0);
assert.strictEqual(node.children[0].exists, true);
assert.strictEqual(calls[1].body.parent_sel, undefined);
}); });
test('empty successful listing keeps the directory present', async () => { test('empty successful listing keeps the directory present', async () => {
+20 -1
View File
@@ -104,7 +104,7 @@ test('an absent directory is marked and its subtree is never fetched', async ()
await pysimFsProbeAll(); await pysimFsProbeAll();
assert.strictEqual(pysimFsTreeRoot.children[0].exists, false); assert.strictEqual(pysimFsTreeRoot.children[0].exists, false);
assert.deepStrictEqual(selectNames(), ['EF.ROOT']); assert.deepStrictEqual(selectNames(), ['EF.ROOT']);
assert.strictEqual(calls.filter(c => c.path === '/api/tree' && c.body.name === 'DF.B').length, 2); assert.strictEqual(calls.filter(c => c.path === '/api/tree' && c.body.name === 'DF.B').length, 1);
const status = els['pysim-fs-probe-status'].textContent; const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /2\/2 files/); assert.match(status, /2\/2 files/);
assert.match(status, /0 present/); assert.match(status, /0 present/);
@@ -122,3 +122,22 @@ test('stop halts the walk and still reports a summary', async () => {
assert.ok(status.startsWith('Stopped —'), status); assert.ok(status.startsWith('Stopped —'), status);
assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files'); assert.strictEqual(els['pysim-fs-probe-btn'].textContent, 'Probe all files');
}); });
test('children of an absent directory are neither fetched nor selected', async () => {
const stale = Object.assign(df('DF.C', '5f03'), {
exists: false,
children: [{ name: 'EF.STALE', fid: '6f0e', isDir: false, exists: true, children: null }],
});
root([stale, ef('EF.ROOT', '2f01')]);
setup([
{ path: '/api/select', name: 'EF.ROOT', reply: { exists: true } },
]);
await pysimFsProbeAll();
assert.deepStrictEqual(selectNames(), ['EF.ROOT']);
assert.strictEqual(stale.exists, false);
assert.strictEqual(calls.filter(c => c.path === '/api/tree').length, 0);
const status = els['pysim-fs-probe-status'].textContent;
assert.match(status, /2\/2 files/);
assert.match(status, /1 present/);
assert.match(status, /1 absent/);
});
+9
View File
@@ -56,3 +56,12 @@ test('collapsed directory hides its children', () => {
const out = pysimFsRenderNode(node, 0); const out = pysimFsRenderNode(node, 0);
assert.ok(!out.includes('EF.IMSI'), out); assert.ok(!out.includes('EF.IMSI'), out);
}); });
test('non-existing directory hides previously loaded children', () => {
const node = df('DF.WLAN', '5f40', { exists: false, expanded: true, children: [ef('EF.UPLMNWLAN', '4f42')] });
const out = pysimFsRenderNode(node, 0);
assert.ok(out.includes('✗'), out);
assert.ok(!out.includes('EF.UPLMNWLAN'), out);
assert.ok(!out.includes('(empty)'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out);
});