ui: named FCI block holds the metadata; hide the empty content pane (v2.7.13)

- pysimFsInfoHtml puts the FID/type/size line inside the fieldset, so the
  selected file is one bordered block with its symbolic name in the border;
  files without an FCI keep the plain borderless line.
- The content pane starts hidden and is synced after every mutation
  (selection clears it, read success shows it, read failure keeps it hidden,
  edit/cancel re-sync): no empty box before a read or edit.
- tests: FCI ordering assertions; new fs_read.test.js (visibility helper,
  read success/failure/throw, markup + call sites); fs_edit extraction.
This commit is contained in:
2026-09-20 23:45:12 +03:00
parent f966276217
commit 93ed870f75
7 changed files with 155 additions and 18 deletions
+2 -1
View File
@@ -27,7 +27,8 @@ let code = 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsSelected = null;\n'
+ 'var pysimFsTreeRoot = null;\n';
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder']) {
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder',
'pysimFsSyncContentVisibility']) {
code += extractFunc(html, fn) + '\n';
}
eval(code);
+120
View File
@@ -0,0 +1,120 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = 'var pysimFsSelected = null;\n'
+ 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsTreeRoot = null;\n';
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
code += extractFunc(html, 'pysimFsRead') + '\n';
eval(code);
let content, status, fetchResult, errorShown;
function fakeEl() {
const classes = new Set();
return {
innerHTML: '',
textContent: '',
classList: {
add: (...cs) => cs.forEach(c => classes.add(c)),
remove: (...cs) => cs.forEach(c => classes.delete(c)),
toggle: (c, on) => { if (on) classes.add(c); else classes.delete(c); },
contains: c => classes.has(c),
},
};
}
function setup() {
content = fakeEl();
status = fakeEl();
globalThis.document = {
getElementById: id => id === 'pysim-fs-content' ? content
: (id === 'pysim-fs-status' ? status : null),
};
globalThis.t = s => s;
globalThis.esc = s => s;
globalThis.pysimFsFindNode = () => ({ name: 'EF.IMSI', fid: '6f07' });
globalThis.pysimFsSelectBody = () => ({});
globalThis.pysimFsShowError = (el, sw, err) => {
errorShown = { sw, err };
el.textContent = 'SW: ' + sw + ' — ' + err;
};
globalThis.pysimFetch = async () => {
if (fetchResult instanceof Error) throw fetchResult;
return fetchResult;
};
pysimFsSelected = 'EF.IMSI';
pysimFsDecodedMode = false;
pysimFsTreeRoot = { name: 'MF' };
fetchResult = { success: true, sw: '9000', data: 'AABB' };
errorShown = null;
}
test('pysimFsSyncContentVisibility hides an empty pane and shows loaded content', () => {
setup();
content.innerHTML = '';
pysimFsSyncContentVisibility();
assert.ok(content.classList.contains('hidden'));
content.innerHTML = ' ';
pysimFsSyncContentVisibility();
assert.ok(content.classList.contains('hidden'), 'whitespace-only content stays hidden');
content.innerHTML = '<textarea>x</textarea>';
pysimFsSyncContentVisibility();
assert.ok(!content.classList.contains('hidden'));
});
test('a successful read fills and shows the content pane', async () => {
setup();
const ok = await pysimFsRead();
assert.strictEqual(ok, true);
assert.ok(content.innerHTML.includes('AABB'));
assert.ok(!content.classList.contains('hidden'));
assert.strictEqual(status.textContent, 'SW: 9000 OK');
});
test('a failed read keeps the content pane hidden', async () => {
setup();
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
const ok = await pysimFsRead();
assert.strictEqual(ok, false);
assert.strictEqual(content.innerHTML, '');
assert.ok(content.classList.contains('hidden'));
assert.deepStrictEqual(errorShown, { sw: '6982', err: 'Security status not satisfied' });
});
test('a thrown read error keeps the content pane hidden', async () => {
setup();
fetchResult = new Error('boom');
const ok = await pysimFsRead();
assert.strictEqual(ok, false);
assert.ok(content.classList.contains('hidden'));
assert.ok(status.textContent.includes('boom'));
});
test('the pane starts hidden and every mutation keeps it in sync', () => {
assert.match(html, /id="pysim-fs-content"[^>]*class="[^"]*\bhidden\b/);
for (const fn of ['pysimFsClickFile', 'pysimFsRead', 'pysimFsEdit', 'pysimFsCancel']) {
assert.ok(extractFunc(html, fn).includes('pysimFsSyncContentVisibility'),
fn + ' must sync the content pane visibility');
}
});
+5 -3
View File
@@ -776,9 +776,11 @@ test('pysimFsInfoHtml shows FID, type, size and the decoded FCI', () => {
// the short file identifier is merged into the file identifier row
assert.ok(out.includes('6F4F · Short file identifier: 22'), out);
assert.ok(!out.includes('<div>Short file identifier:'), out);
// the FCI sits in a bordered container whose legend is the symbolic name,
// below the metadata line
assert.ok(out.indexOf('FID: 6F4F') < out.indexOf('<fieldset'), out);
// the metadata line and the FCI rows share one bordered container whose
// legend is the symbolic name
assert.ok(out.indexOf('<fieldset') < out.indexOf('<legend'), out);
assert.ok(out.indexOf('<legend') < out.indexOf('FID: 6F4F'), out);
assert.ok(out.indexOf('FID: 6F4F') < out.indexOf('File descriptor'), out);
assert.ok(out.includes('<legend class="px-1 text-xs font-mono font-medium text-gray-500 dark:text-slate-400">EF.SAMPLE</legend>'), out);
delete global.t;
});