@@ -1413,7 +1413,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
-const SIMPLE_VERSION = '2.7.12';
+const SIMPLE_VERSION = '2.7.13';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -7161,16 +7161,18 @@ function pysimFsInfoHtml(sel, name) {
if (num(sel.file_size) !== null) attrs.push(esc(t('Size')) + ': ' + esc(num(sel.file_size)));
if (num(sel.record_len) !== null) attrs.push(esc(t('Record length')) + ': ' + esc(num(sel.record_len)));
if (num(sel.num_of_rec) !== null) attrs.push(esc(t('Record count')) + ': ' + esc(num(sel.num_of_rec)));
- let html = '';
- if (attrs.length) html += '
' + attrs.join(' · ') + '
';
- if (sel.fci_hex) {
- html += '';
+ const metaCls = 'text-xs font-mono text-gray-500 dark:text-slate-400';
+ if (!sel.fci_hex) {
+ return attrs.length ? '
' + attrs.join(' · ') + '
' : '';
}
- return html;
+ // One bordered block per selected file: the metadata line and the decoded
+ // FCI together, with the symbolic name embedded in the border.
+ return '';
}
async function pysimFsClickFile(name) {
@@ -7187,6 +7189,7 @@ async function pysimFsClickFile(name) {
pysimFsSelected = name;
document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel, name) : '';
document.getElementById('pysim-fs-content').innerHTML = '';
+ pysimFsSyncContentVisibility();
document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card';
if (exists) {
document.getElementById('pysim-fs-detail').classList.remove('hidden');
@@ -7197,6 +7200,13 @@ async function pysimFsClickFile(name) {
pysimRefresh();
}
+// The content pane only appears once a read or edit has something to show;
+// an empty container would just be an empty box.
+function pysimFsSyncContentVisibility() {
+ const out = document.getElementById('pysim-fs-content');
+ if (out) out.classList.toggle('hidden', !out.innerHTML.trim());
+}
+
async function pysimFsRead() {
if (!pysimFsSelected) return false;
const rawName = pysimFsSelected;
@@ -7233,6 +7243,8 @@ async function pysimFsRead() {
} catch (e) {
statusEl.textContent = 'Error: ' + e.message;
return false;
+ } finally {
+ pysimFsSyncContentVisibility();
}
}
@@ -7317,6 +7329,7 @@ async function pysimFsEdit() {
});
});
pysimFsEditData = out.innerHTML;
+ pysimFsSyncContentVisibility();
}
function pysimFsCancel() {
@@ -7328,6 +7341,7 @@ function pysimFsCancel() {
const out = document.getElementById('pysim-fs-content');
if (pysimFsEditData) out.innerHTML = pysimFsEditData;
pysimFsEditData = null;
+ pysimFsSyncContentVisibility();
}
async function pysimFsSave() {
diff --git a/frontend/sw.js b/frontend/sw.js
index 2cca930..6b95b70 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v215';
+const CACHE = 'simple-v216';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/fs_edit.test.js b/frontend/tests/fs_edit.test.js
index 6a7a7c6..f9f6239 100644
--- a/frontend/tests/fs_edit.test.js
+++ b/frontend/tests/fs_edit.test.js
@@ -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);
diff --git a/frontend/tests/fs_read.test.js b/frontend/tests/fs_read.test.js
new file mode 100644
index 0000000..2bfb0a1
--- /dev/null
+++ b/frontend/tests/fs_read.test.js
@@ -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 = '';
+ 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');
+ }
+});
diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js
index 7ee2ef6..f095044 100644
--- a/frontend/tests/profiler.test.js
+++ b/frontend/tests/profiler.test.js
@@ -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('
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('