ui: gate controls by server/card state; move Custom files to Profiler
Availability model: server-down / server-up-no-card / card-equipped. - header shows a symbolic indicator dot (red/yellow/green, tooltip) - a 5s probe tracks /api/version+/api/status while not fully ready; the 2s status poll takes over once connected - controls declare data-needs='server' or 'card'; disabled + tooltip when the state does not satisfy them: equip (server), status/reset, file manager read/save/edit, raw APDU, profile-from-card, new snapshot, profiler list Check card, event Send, RAM install/explore delete, STK menu, Send STATUS, Verify vs pySim (server), pySim execute (server), PLI save/poll toggle (server) - pysimConnect no longer conflates server and card: it records both and Connect stays usable without a card - ramSendOta/ramInstallCap now use pysimFetch instead of raw fetch (relative /api URLs broke custom server URLs) Custom files moved from the Card reader sub-tabs to the Profiler list tabs (Profiles / Card snapshots / Custom files); help/README/AGENTS updated. Tests: availability gating, indicator structure, moved tab. SW cache v103 -> v104.
This commit is contained in:
@@ -21,8 +21,11 @@ function extractFunc(src, name) {
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n';
|
||||
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
|
||||
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n';
|
||||
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
|
||||
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
|
||||
code += extractFunc(html, 'pysimControlDisabled') + '\n';
|
||||
code += '\nglobalThis.esc = s => s;\n';
|
||||
code += 'globalThis.t = s => s;\n';
|
||||
eval(code);
|
||||
@@ -32,9 +35,10 @@ function setup() {
|
||||
const calls = { connected: [], resets: [], refreshStatus: [] };
|
||||
_pysimCardStateKey = null;
|
||||
_pysimCardSession = null;
|
||||
globalThis.document = { getElementById: () => el };
|
||||
globalThis.document = { getElementById: () => el, querySelectorAll: () => [] };
|
||||
globalThis.pysimSetConnected = v => calls.connected.push(v);
|
||||
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
|
||||
globalThis.pysimApplyAvailability = () => {};
|
||||
return { el, calls };
|
||||
}
|
||||
|
||||
@@ -98,3 +102,21 @@ test('payload without connected flag is ignored', () => {
|
||||
pysimCardStateUpdate(null);
|
||||
assert.deepStrictEqual(calls.connected, []);
|
||||
});
|
||||
|
||||
test('availability state and control gating follow server/card state', () => {
|
||||
_pysimServerAvailable = null;
|
||||
assert.strictEqual(pysimAvailabilityState(), 'server-down');
|
||||
assert.strictEqual(pysimControlDisabled('server', 'server-down'), true);
|
||||
assert.strictEqual(pysimControlDisabled('card', 'server-down'), true);
|
||||
|
||||
_pysimServerAvailable = true;
|
||||
_pysimCardEquipped = false;
|
||||
assert.strictEqual(pysimAvailabilityState(), 'no-card');
|
||||
assert.strictEqual(pysimControlDisabled('server', 'no-card'), false);
|
||||
assert.strictEqual(pysimControlDisabled('card', 'no-card'), true);
|
||||
|
||||
_pysimCardEquipped = true;
|
||||
assert.strictEqual(pysimAvailabilityState(), 'card');
|
||||
assert.strictEqual(pysimControlDisabled('card', 'card'), false);
|
||||
assert.strictEqual(pysimControlDisabled('server', 'card'), false);
|
||||
});
|
||||
|
||||
@@ -41,3 +41,10 @@ test('scan name input starts scanning on Enter', () => {
|
||||
test('snapshot view has a timing summary block', () => {
|
||||
assert.ok(html.includes('id="snapshot-summary"'));
|
||||
});
|
||||
|
||||
test('header state indicator and profiler custom-files tab', () => {
|
||||
assert.ok(html.includes('id="state-indicator"'));
|
||||
assert.ok(html.includes('id="profiler-list-custom"'));
|
||||
assert.ok(html.includes('data-list-tab="custom"'));
|
||||
assert.ok(!html.includes('data-pysim-sub="custom"'));
|
||||
});
|
||||
|
||||
@@ -921,12 +921,15 @@ test('profilerListSwitch toggles the profiles/snapshots tabs', () => {
|
||||
dataset: { listTab: tab },
|
||||
classList: { _c: new Set(), toggle(c, on) { if (on) this._c.add(c); else this._c.delete(c); } },
|
||||
});
|
||||
const btns = [mkBtn('profiles'), mkBtn('snapshots')];
|
||||
const btns = [mkBtn('profiles'), mkBtn('snapshots'), mkBtn('custom')];
|
||||
const profilesEl = { hidden: false, classList: { toggle(cls, on) { profilesEl.hidden = on; } } };
|
||||
const snapsEl = { hidden: true, classList: { toggle(cls, on) { snapsEl.hidden = on; } } };
|
||||
const customEl = { hidden: true, classList: { toggle(cls, on) { customEl.hidden = on; } } };
|
||||
global.pysimCustomRender = () => {};
|
||||
global.setHelpAnchor = () => {};
|
||||
global.document = {
|
||||
querySelectorAll: sel => (sel === '.profiler-list-tab' ? btns : []),
|
||||
getElementById: id => (id === 'profiler-list-profiles' ? profilesEl : id === 'profiler-list-snapshots' ? snapsEl : null),
|
||||
getElementById: id => (id === 'profiler-list-profiles' ? profilesEl : id === 'profiler-list-snapshots' ? snapsEl : id === 'profiler-list-custom' ? customEl : null),
|
||||
};
|
||||
|
||||
profilerListSwitch('snapshots');
|
||||
@@ -941,7 +944,14 @@ test('profilerListSwitch toggles the profiles/snapshots tabs', () => {
|
||||
assert.ok(btns[0].classList._c.has('bg-blue-600'));
|
||||
assert.ok(btns[1].classList._c.has('bg-gray-200'));
|
||||
|
||||
profilerListSwitch('custom');
|
||||
assert.strictEqual(customEl.hidden, false);
|
||||
assert.strictEqual(profilesEl.hidden, true);
|
||||
assert.strictEqual(snapsEl.hidden, true);
|
||||
|
||||
delete global.document;
|
||||
delete global.pysimCustomRender;
|
||||
delete global.setHelpAnchor;
|
||||
});
|
||||
|
||||
test('profilerScanRefreshOptions re-translates mask labels without touching checkbox state', () => {
|
||||
|
||||
Reference in New Issue
Block a user