337e5df770
SET UP MENU / SELECT ITEM items carry an optional Items Next Action Indicator (TS 102 223 8.24, tag '18'): one byte per item, in list order, coded with the Table 9.4 values marked "Used for Next Action Indicator". The decoder ignored it, so neither the proactive log nor the STK menu showed what the card would do when an item is selected. - server: NAI_TYPES whitelist (the ToC-only values - e.g. '26', '27', '47' - are reserved there and are ignored, as are '00' and unlisted values), _nai_name(), _attach_nai() (a short NAI list leaves the tail without an indicator, extra bytes are ignored); _parse_select_item() and _parse_setup_menu_items() plus the TERMINAL PROFILE SET UP MENU walk attach nai/nai_name to the items; _decode_cmd() renders '1. Menu -> SET UP MENU'. - PWA: stkMenuNaiSuffix() adds a small gray '<name>' suffix to STK menu items that carry an NAI; the proactive log picks the decode up via cmd_decoded. - tests: SET UP MENU / SELECT ITEM vectors with NAIs, a reserved value that is ignored, a short NAI list, and the stkMenuNaiSuffix unit test. - docs: UICC_SPECS 6.5 gained the '18' Items next action indicator row (8.24/9.4); help EN/RU mention the menu suffix; AGENTS files updated. 548 frontend / 413 Python green; version 3.5.3; sw cache simple-v255.
95 lines
3.2 KiB
JavaScript
95 lines
3.2 KiB
JavaScript
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, asyncFn) {
|
|
const re = new RegExp('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 (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
|
}
|
|
|
|
let code = extractFunc(html, 'stkMenuRespond', true) + '\n';
|
|
code += extractFunc(html, 'stkMenuNaiSuffix') + '\n';
|
|
code += 'globalThis.esc = s => s;\n';
|
|
eval(code);
|
|
|
|
function setup(response) {
|
|
const calls = { handled: null, rendered: 0 };
|
|
globalThis.pysimFetch = async () => response;
|
|
globalThis.stkMenuHandleResponse = d => { calls.handled = d; };
|
|
globalThis.stkMenuRenderItems = () => { calls.rendered++; };
|
|
const btns = { classList: { add: () => {} } };
|
|
const back = { style: {} };
|
|
globalThis.document = {
|
|
getElementById: id => (id === 'stk-menu-buttons' ? btns : id === 'stk-back-btn' ? back : { innerHTML: '' }),
|
|
};
|
|
return calls;
|
|
}
|
|
|
|
test('the STK menu item suffix shows the item next action (8.24)', () => {
|
|
assert.strictEqual(stkMenuNaiSuffix({ id: 1, text: 'Menu', nai: 0x25, nai_name: 'SET UP MENU' }),
|
|
'\u25b8 SET UP MENU');
|
|
// no NAI (or a reserved one, which the server drops) -> no suffix
|
|
assert.strictEqual(stkMenuNaiSuffix({ id: 2, text: 'Info' }), '');
|
|
assert.strictEqual(stkMenuNaiSuffix(null), '');
|
|
});
|
|
|
|
test('back with a fetched SELECT ITEM continues the card dialogue', async () => {
|
|
const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] };
|
|
const calls = setup(data);
|
|
await stkMenuRespond('back');
|
|
assert.strictEqual(calls.handled, data);
|
|
assert.strictEqual(calls.rendered, 0);
|
|
});
|
|
|
|
test('back with a fetched DISPLAY TEXT shows it', async () => {
|
|
const data = { type: 'display_text', text: 'hello' };
|
|
const calls = setup(data);
|
|
await stkMenuRespond('back');
|
|
assert.strictEqual(calls.handled, data);
|
|
assert.strictEqual(calls.rendered, 0);
|
|
});
|
|
|
|
test('timeout with a fetched SELECT ITEM continues the card dialogue', async () => {
|
|
const data = { type: 'select_item', items: [] };
|
|
const calls = setup(data);
|
|
await stkMenuRespond('timeout');
|
|
assert.strictEqual(calls.handled, data);
|
|
assert.strictEqual(calls.rendered, 0);
|
|
});
|
|
|
|
test('back answered with SW 9000 falls back to the cached top menu', async () => {
|
|
const calls = setup({ type: 'done', sw: '9000' });
|
|
await stkMenuRespond('back');
|
|
assert.strictEqual(calls.handled, null);
|
|
assert.strictEqual(calls.rendered, 1);
|
|
});
|
|
|
|
test('cancel falls back to the cached top menu', async () => {
|
|
const calls = setup({ sw: '9000' });
|
|
await stkMenuRespond('cancel');
|
|
assert.strictEqual(calls.handled, null);
|
|
assert.strictEqual(calls.rendered, 1);
|
|
});
|
|
|
|
test('ok navigates with the server response', async () => {
|
|
const data = { type: 'select_item', items: [{ id: 1, text: 'x' }] };
|
|
const calls = setup(data);
|
|
await stkMenuRespond('ok');
|
|
assert.strictEqual(calls.handled, data);
|
|
assert.strictEqual(calls.rendered, 0);
|
|
});
|